इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

List

List लंबा या असीमित डेटा दिखाता है। केवल व्यूपोर्ट के पास की पंक्तियाँ (rows) मौजूद रहती हैं: जैसे-जैसे उपयोगकर्ता स्क्रॉल करता है, बाहर जाने वाली पंक्तियाँ रीसाइकल होकर अंदर आने वाली पंक्तियों के काम आती हैं। कौन-सी पंक्तियाँ मौजूद हैं, उनकी स्थिति, कॉलम, sticky हेडर और viewability, यह सब नेटिव रूप से कंप्यूट होता है, इसलिए हज़ारों आइटम वाली सूची को स्क्रॉल करने की लागत बीस आइटम वाली सूची जितनी ही होती है। Rust में इसे virtual_list(count, |at| row) से बनाया जाता है, और TSX में nsh से <List> द्वारा।

el-list.tsx
el-list.tsxLive
import { List, type ListRef, mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, size, tint, ui, wide } from "./palette";

type Trade = { id: string; symbol: string; shares: number; change: number };

const symbols = ["AAPL", "NVDA", "TSLA", "MSFT", "AMZN", "META", "GOOG", "AMD", "NFLX", "INTC"];
const LIMIT = 200;

// A page of made-up trades; ids keep counting so every row has its own key.
let next = 0;
const page = (n: number): Trade[] =>
  Array.from({ length: n }, () => {
    const i = next++;
    return { id: `t${i}`, symbol: symbols[(i * 7) % symbols.length], shares: 10 + ((i * 37) % 290), change: (((i * 53) % 200) - 100) / 10 };
  });

function App() {
  const trades = signal(page(40));
  const loading = signal(false);
  const loads = signal(0);
  const refreshing = signal(false);
  const visible = signal("");
  let list!: ListRef;

  const loadMore = () => {
    if (loading() || trades().length >= LIMIT) return;
    loading.set(true);
    setTimeout(() => {
      trades.set([...trades(), ...page(20)]);
      loads.update((n) => n + 1);
      loading.set(false);
    }, 600);
  };

  const refresh = () => {
    refreshing.set(true);
    setTimeout(() => {
      next = 0;
      trades.set(page(40));
      loads.set(0);
      refreshing.set(false);
    }, 800);
  };
  const viewable = ({ viewableItems }: { viewableItems: { index: number }[] }) =>
    visible.set(viewableItems.length ? `${viewableItems[0].index}–${viewableItems[viewableItems.length - 1].index}` : "");
  const toIndex = () => list.scrollToIndex({ index: Math.min(100, trades().length - 1), viewPosition: 0.5 });
  const toEnd = () => list.scrollToEnd({ animated: true });
  const status = () => (loading() ? "Loading 20 more…" : trades().length >= LIMIT ? "The end" : "");
  const changeColor = (t: Trade) => (t.change >= 0 ? ui().up : ui().down);

  // A phone: cards in one column, pull to refresh.
  const cards = () => (
    <View style={{ flex: 1, backgroundColor: ui().page, paddingTop: 24 }}>
      <View style={{ paddingHorizontal: 16, gap: 6, paddingBottom: 8 }}>
        <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>list</Text>
        <Text style={{ color: ui().muted }}>
          {trades().length} rows · onLoadMore {loads()}× · visible {visible()}
        </Text>
      </View>
      <List
        ref={list}
        testID="trades"
        each={trades()}
        keyExtractor={(t) => t.id}
        numColumns={1}
        estimatedItemSize={64}
        style={{ flex: 1 }}
        contentStyle={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 8 }}
        onLoadMore={loadMore}
        loadMoreThreshold={0.5}
        refreshing={refreshing()}
        onRefresh={refresh}
        viewabilityConfig={{ itemVisibleThreshold: 0.5 }}
        onViewableItemsChange={viewable}
        header={
          <View style={{ ...ui().card, borderRadius: 14, padding: 14, gap: 10, marginBottom: 4 }}>
            <Text style={{ color: ui().muted }}>
              {desktop ? "Scroll with the wheel or trackpad" : "Swipe up; pull down to refresh"}: more rows load near the end, up to {LIMIT}.
            </Text>
            <View style={{ flexDirection: "row", gap: 8 }}>
              <Chip label="scrollToIndex 100" onPress={toIndex} />
              <Chip label="scrollToEnd" onPress={toEnd} />
            </View>
          </View>
        }
        footer={<Text style={{ color: ui().faint, textAlign: "center", paddingVertical: 16 }}>{status()}</Text>}
      >
        {(trade, index) => (
          <View style={{ ...ui().row, flex: 1, flexDirection: "row", alignItems: "center", gap: 12, padding: 14, borderRadius: 12 }}>
            <Text style={{ color: ui().faint, width: 32, fontVariant: ["tabular-nums"] }}>{index()}</Text>
            <View style={{ flex: 1 }}>
              <Text style={{ color: ui().text, fontWeight: "600" }}>{trade().symbol}</Text>
              <Text style={{ color: ui().muted, fontSize: 12 }}>
                {trade().shares} shares · key {trade().id}
              </Text>
            </View>
            <Text
              style={{
                color: changeColor(trade()),
                backgroundColor: tint(changeColor(trade()), 0.12),
                paddingHorizontal: 8,
                paddingVertical: 3,
                borderRadius: 6,
                overflow: "hidden",
                fontWeight: "600",
                fontVariant: ["tabular-nums"],
              }}
            >
              {trade().change >= 0 ? "+" : ""}
              {trade().change.toFixed(1)}%
            </Text>
          </View>
        )}
      </List>
    </View>
  );

  // A wide window: a table with fixed column lanes, its controls beside it.
  const cell = { fontSize: 13, fontVariant: ["tabular-nums" as const] };
  const lanes = { index: 48, shares: 88, key: 104, change: 88 };
  const table = () => (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>list</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Scroll with the wheel or trackpad" : "Swipe up"}: more rows load near the end, up to {LIMIT}.
      </Text>
      <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
        <View style={{ ...ui().card, flex: 1, minWidth: 0, borderRadius: 12, overflow: "hidden" }}>
          <View
            style={{
              flexDirection: "row",
              alignItems: "center",
              gap: 12,
              height: size.rowSm,
              paddingHorizontal: 12,
              borderBottomWidth: 1,
              borderBottomColor: ui().border,
              backgroundColor: ui().inset.backgroundColor,
            }}
          >
            <Text style={{ ...cell, width: lanes.index, textAlign: "right", fontSize: 12, fontWeight: "600", color: ui().muted }}>#</Text>
            <Text style={{ ...cell, flex: 1, fontSize: 12, fontWeight: "600", color: ui().muted }}>Symbol</Text>
            <Text style={{ ...cell, width: lanes.shares, textAlign: "right", fontSize: 12, fontWeight: "600", color: ui().muted }}>Shares</Text>
            <Text style={{ ...cell, width: lanes.key, paddingStart: 16, fontSize: 12, fontWeight: "600", color: ui().muted }}>Key</Text>
            <Text style={{ ...cell, width: lanes.change, textAlign: "right", fontSize: 12, fontWeight: "600", color: ui().muted }}>Change</Text>
          </View>
          <List
            ref={list}
            testID="trades"
            each={trades()}
            keyExtractor={(t) => t.id}
            estimatedItemSize={size.row}
            style={{ flex: 1 }}
            onLoadMore={loadMore}
            loadMoreThreshold={0.5}
            refreshing={refreshing()}
            onRefresh={refresh}
            viewabilityConfig={{ itemVisibleThreshold: 0.5 }}
            onViewableItemsChange={viewable}
            footer={<Text style={{ color: ui().faint, fontSize: 13, textAlign: "center", paddingVertical: 12 }}>{status()}</Text>}
          >
            {(trade, index) => (
              <View
                style={{
                  flexDirection: "row",
                  alignItems: "center",
                  gap: 12,
                  height: size.row - 4,
                  paddingHorizontal: 12,
                  borderBottomWidth: 1,
                  borderBottomColor: ui().line,
                }}
              >
                <Text style={{ ...cell, width: lanes.index, textAlign: "right", color: ui().faint }}>{index()}</Text>
                <Text style={{ ...cell, flex: 1, fontWeight: "600", color: ui().text }}>{trade().symbol}</Text>
                <Text style={{ ...cell, width: lanes.shares, textAlign: "right", color: ui().text }}>{trade().shares}</Text>
                <Text style={{ ...cell, width: lanes.key, paddingStart: 16, color: ui().muted }}>{trade().id}</Text>
                <Text style={{ ...cell, width: lanes.change, textAlign: "right", fontWeight: "600", color: changeColor(trade()) }}>
                  {trade().change >= 0 ? "+" : ""}
                  {trade().change.toFixed(1)}%
                </Text>
              </View>
            )}
          </List>
        </View>
        <Inspector>
          <Panel title="rows · onLoadMore · visible">
            <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
              {trades().length} rows · {loads()}× · visible {visible()}
            </Text>
          </Panel>
          <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
            <Chip label="scrollToIndex 100" onPress={toIndex} />
            <Chip label="scrollToEnd" onPress={toEnd} />
          </View>
        </Inspector>
      </View>
    </View>
  );

  return <>{() => (wide() ? table() : cards())}</>;
}

function Chip(props: { label: string; onPress: () => void }) {
  return (
    <View onPress={props.onPress} {...button()}>
      <Text style={buttonText()}>{props.label}</Text>
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

#[derive(Clone, PartialEq)]
struct Contact {
    id: usize,
    name: String,
}

fn app() -> impl IntoElement {
    let contacts = signal(
        (0..1000).map(|i| Contact { id: i, name: format!("Contact {}", i + 1) }).collect::<Vec<_>>(),
    );
    virtual_list(move || contacts.with(|c| c.len()), move |at| {
        view().h(px(56.0)).px_4().justify_center().child(
            text(move || contacts.with(|c| format!("{}. {}", at.index() + 1, c[at.index()].name)))
                .text_base(),
        )
    })
    .estimated_item_size(px(56.0))
    .flex_1()
}

TSX में List जेनेरिक है: List<T> जाँचता है कि each और row function आइटम के type पर एकमत हैं, जिसे TypeScript each से अनुमानित करता है (List API देखें)। Rust में virtual_list पंक्तियों की संख्या लेता है (एक value, एक signal या एक closure: सूची अपनी जगह पर बढ़ती और घटती है) और वह function लेता है जो Position पर पंक्ति बनाता है; पंक्तियाँ component का डेटा at.index() से पढ़ती हैं।

छोटी सूची के लिए, जिसकी सभी पंक्तियाँ मौजूद रहनी चाहिए (settings पेज, फ़ॉर्म), scroll view के भीतर Each अधिक सरल है। grouped table की तरह सजाई गई सूची के लिए settings-style list देखें। lists guide में patterns दिए गए हैं: infinite loading, chat, sections।

#पंक्तियाँ रीसाइकल होती हैं

Row function हर आइटम के लिए नहीं, बल्कि हर cell के लिए एक बार चलता है। जब किसी cell को दूसरे आइटम के लिए दोबारा इस्तेमाल किया जाता है, तो उसकी स्थिति बदल जाती है (TSX: item() और index() accessors; Rust: at.index()) और उसे पढ़ने वाली हर चीज़ अपडेट हो जाती है:

virtual_list(move || messages.with(|m| m.len()), move |at| {
    text(move || messages.with(|m| m[at.index()].body.clone()))
})

इसलिए हर आइटम की state को अपने डेटा में रखें (id के अनुसार), न कि row function के अंदर बनाए गए signals में: ऐसा signal cell के साथ रहता है और किसी दूसरे आइटम पर दिखने लगता है।

अपने अगले आइटम की प्रतीक्षा कर रहे cells तब छिपा दिए जाते हैं जब उनकी पंक्ति जा चुकी हो या कोई दूसरा cell उसे दिखा रहा हो, ताकि screen readers, touches और UI tests को हर पंक्ति एक ही बार मिले।

#Props

PropRustTypeडिफ़ॉल्टविवरण
eachvirtual_list(count, ..)readonly T[]आइटम।
childrenvirtual_list(.., |at| row)(item: () => T, index: () => number) => JSX.Elementएक पंक्ति रेंडर करता है।
estimatedItemSize.estimated_item_size(px(..))number50अभी तक न नापे गए आइटमों का आकार, points में। सटीक अनुमान स्क्रॉल बार को स्थिर रखते हैं।
horizontal.horizontal()booleanfalseबगल की ओर स्क्रॉल करता है।
viewport.viewport(ListViewport::..)"own" | "parent""own""own": सूची अपने ही scroll view में स्क्रॉल होती है। "parent": वह अपने parent के लेआउट में अपनी पूरी लंबाई में रहती है और अपने आस-पास के scroll view के व्यूपोर्ट के पास की पंक्तियाँ रेंडर करती है; वही scroll view उसे बाकी पेज के साथ स्क्रॉल करता है (पेज में lists देखें)। सूची बनाते समय चुना जाता है।
numColumns.columns(n)number1प्रति पंक्ति आइटम (vertical lists)।
inverted.inverted()booleanfalseअंत से लेआउट होती है, आइटम 0 सबसे अंत में (chat style): अंत तक स्क्रॉल होकर शुरू होती है और सामग्री बदलने पर अंत से अपनी दूरी बनाए रखती है।
maintainVisibleContentPosition.follow_end(px(threshold)){ autoscrollToBottomThreshold?: number }inverted न होने वाली सूचियों के लिए: सामग्री के अंत पर, या उससे इतने points के भीतर, सामग्री बढ़ने या व्यूपोर्ट घटने पर सूची अंत पर ही रहती है; और ऊपर स्क्रॉल होने पर दिखती हुई सामग्री अपनी जगह रहती है।
stickyHeaderIndices.sticky_header_indices([..])number[]वे आइटम जो स्क्रॉल होकर आगे निकल जाने पर शुरुआत से चिपके रहते हैं (एक कॉलम, inverted नहीं)।
contentStyle.content_padding(..), .content_padding_x(..), .row_gap(..), .column_gap(..), …ListContentStyleसामग्री की padding और gaps, points में: padding, paddingHorizontal, paddingVertical, paddingTop, paddingRight, paddingBottom, paddingLeft, gap, rowGap, columnGap।
contentClassstringसामग्री की padding और gaps classes के रूप में (p-*, gap-*); contentStyle इन पर भारी पड़ता है।
header.header(el)ListSlotआइटमों से पहले: एक component (Header) या एक element (<Header />)।
footer.footer(el)ListSlotआइटमों के बाद।
empty.empty(el)ListSloteach के खाली होने पर आइटमों की जगह दिखता है।
separator.separator(|| el)ListSlotआइटमों के बीच (एक कॉलम)।
initialScrollIndex.initial_scroll_index(i)numberपहले फ़्रेम से पहले इस आइटम तक स्क्रॉल करता है।
keyExtractor.keys(|| ids)(item: T, index: number) => stringitem.key, item.id या indexviewability tokens में आइटम की key। Rust में, पंक्तियों की पहचान, ताकि उनसे पहले की पंक्तियाँ आने-जाने पर भी पंक्तियाँ अपने cells और sizes बनाए रखें।
onLoadMore.on_load_more(..)() => voidअंत पास है: और लोड करें।
loadMoreThreshold.load_more_threshold(viewports)number1अंत से कितना पास (viewports में) होने पर onLoadMore चलता है।
onLoadMoreAtStart.on_load_more_at_start(..)() => voidशुरुआत पास है, उपयोगकर्ता या किसी scroll command द्वारा सूची हिलाए जाने के बाद: पहले आइटम से पहले का डेटा लोड करें।
loadMoreAtStartThreshold.load_more_at_start_threshold(viewports)number1शुरुआत से कितना पास (viewports में) होने पर onLoadMoreAtStart चलता है।
onViewableItemsChange.on_viewable_items_change(..)(info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => voidआइटम viewable हुए या viewable नहीं रहे।
viewabilityConfig.viewability(..), .minimum_view_time(..)ViewabilityConfigआइटम कब viewable माने जाते हैं (नीचे)।
refreshing.refreshing(..)booleanpull-to-refresh spinner दिखता है।
onRefresh.on_refresh(..)() => voidउपयोगकर्ता ने refresh करने के लिए नीचे खींचा।
refreshIndicator.refresh_indicator(Some(height)){ height: number }ऐप अपना indicator खुद बनाता है, height points ऊँचा: platform का spinner छिपा रहता है, खींचने पर height तक पहुँचते ही refresh होता है, और refresh के दौरान सामग्री के ऊपर height जगह बनी रहती है। 0 या उससे कम ऊँचाई का मतलब है कि कोई indicator नहीं।
pullValue.pull_value(&value)AnimatedValueएक animated value, जिसमें यह मान भरा जाता है कि सूची को उसके शीर्ष से आगे कितना खींचा गया है (विश्राम में 0, खिंचाव के साथ बढ़ता है; refresh के दौरान सामग्री के ऊपर रखी जगह: height के साथ refreshIndicator), ऐप के indicator के लिए।
scrollbar.scrollbar(..)boolean | ScrollbarThemetruescroll indicator दिखता है: platform का, या ऐप द्वारा theme सेट करने पर ऐप का built-in bar; object का मतलब है view की अपनी theme। डेस्कटॉप और desktop browsers में horizontal view इसे बनाए रखता है: यही माउस का scroll bar है।
scrollValue.scroll_value(&value)AnimatedValuescroll offset से भरा जाने वाला animated value, scroll-linked animations के लिए।
keyboardDismissMode.keyboard_dismiss_mode(..)"none" | "on-drag" | "interactive""none"क्या खींचने (drag) से keyboard बंद होता है।
styleStyled methodsStyleProp<ViewStyle>सूची की अपनी style (इसे सीमित आकार चाहिए: flex: 1 या height)।
testID.test_id(id)stringवह id जिससे UI tests सूची का scroll view ढूँढते हैं (nsh test का id:, XCUITest, UI Automator)।
ref.element_ref(&handle)ListRef | (node: ListRef) => voidसूची का ref (ref methods देखें)।

Lists Scroll के scrolling props भी लेती हैं (Rust में Scrollable methods): pagingEnabled, snapToInterval, snapToOffsets, snapToAlignment, disableIntervalMomentum, decelerationRate, bounces, contentInset, contentOffset, keyboardShouldPersistTaps, scrollEventThrottle और scrollable। Lists अपना scale बनाए रखती हैं।

हर element जो props लेता है (common props) वे जस के तस सूची के scroll view तक पहुँचते हैं: testID, accessibilityLabel और अन्य accessibility props, styles, onLayout, touch और responder props। UI tests सूची को उसके testID से वैसे ही ढूँढते हैं जैसे किसी भी view को।

ViewabilityConfig (Rust: Viewability { item_visible_threshold, view_area_coverage_threshold } और .minimum_view_time(..)):

FieldTypeविवरण
itemVisibleThresholdnumberआइटम का कितना हिस्सा दिखना चाहिए, 0 से 1 तक।
viewAreaCoverageThresholdnumberआइटम को व्यूपोर्ट का कितना हिस्सा घेरना चाहिए, 0 से 1 तक। दोनों में से कोई भी threshold न होने पर कोई भी दिखता हुआ pixel गिना जाता है।
minimumViewTimenumberरिपोर्ट होने से पहले आइटमों को कितने मिलीसेकंड viewable बने रहना चाहिए।

ViewToken<T> है { item, key, index, isViewable }। Rust के listener को ViewableItems मिलता है: indices() viewable पंक्तियों की range है, changed() वे पंक्तियाँ हैं जो आईं या गईं।

#Events

EventRustPayloadकब
onScroll.on_scrollScrollEventoffset बदला।
onScrollDragStart / onScrollDragEnd.on_scroll_drag_start / .on_scroll_drag_end{ offset }खींचना (drag) शुरू हुआ / रुका।
onMomentumStart / onMomentumEnd.on_momentum_start / .on_momentum_end{ offset }glide शुरू हुआ / रुका।
onScrollEnd.on_scroll_end{ offset }स्क्रॉल रुक गया।
onLoadMore.on_load_moreकोई नहींअंत से loadMoreThreshold viewports के भीतर।
onLoadMoreAtStart.on_load_more_at_startकोई नहींशुरुआत से loadMoreAtStartThreshold viewports के भीतर, स्क्रॉल के बाद हर बार पास आने पर एक बार।
onViewableItemsChange.on_viewable_items_change{ viewableItems, changed } (ViewableItems)viewability बदली।
onRefresh.on_refreshकोई नहींrefresh के लिए खींचा गया।

#Ref methods

MethodRustविवरण
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })scroll_to_index(index, percentage(0.5), px(0.0), animated)आइटम index को दृश्य में स्क्रॉल करता है। viewPosition उसकी जगह तय करता है: 0 व्यूपोर्ट की शुरुआत में (डिफ़ॉल्ट), 0.5 बीच में, 1 अंत में; viewOffset उसे और खिसकाता है। मौजूदा (नापे गए या अनुमानित) आइटम आकारों के साथ नेटिव रूप से हल होता है।
scrollToOffset({ offset, animated? })scroll_to_offset(px(..), animated)सूची के axis के अनुदिश किसी offset तक स्क्रॉल करता है।
scrollToEnd({ animated? })scroll_to_end(animated)सामग्री के अंत तक स्क्रॉल करता है (inverted सूची में शुरुआत तक)।
scrollTo({ x?, y?, animated? })scroll_to(point(..), animated)किसी offset तक स्क्रॉल करता है।

#उदाहरण

हेडर और viewability वाला एक grid:

let visible = signal(SharedString::default());
let list = element_ref();

virtual_list(move || photos.with(|p| p.len()), move |at| {
    image(move || photos.with(|p| p[at.index()].url.clone())).h(px(120.0)).rounded(px(8.0))
})
.element_ref(&list)
.columns(3)
.estimated_item_size(px(120.0))
.content_padding(px(12.0))
.row_gap(px(8.0))
.column_gap(px(8.0))
.viewability(Viewability { item_visible_threshold: Some(Percentage(0.5)), ..Viewability::default() })
.on_viewable_items_change(move |items: &ViewableItems| {
    visible.set(format!("{} visible", items.indices().count()).into())
})
.header(
    text("Jump to 60")
        .on_press(move || list.scroll_to_index(60, percentage(0.5), px(0.0), true)),
)
.flex_1()

एक chat:

virtual_list(move || messages.with(|m| m.len()), move |at| bubble(messages, at))
    .keys(move || messages.with(|m| m.iter().map(|m| m.id).collect::<Vec<_>>()))
    .inverted()
    .estimated_item_size(px(64.0))
    .keyboard_dismiss_mode(KeyboardDismissMode::Interactive)
    .flex_1()

आइटम 0 सबसे नया संदेश है, जो नीचे दिखता है; items के आगे जोड़े गए नए संदेश बिना उस चीज़ को हिलाए दिखते हैं जिसे उपयोगकर्ता पढ़ रहा है।

#Platform नोट्स

  • दिखती हुई पंक्तियों के ऊपर आइटम जोड़ने पर दोनों platforms पर दिखती हुई पंक्तियाँ अपनी जगह रहती हैं।
  • right-to-left लेआउट में horizontal lists नेटिव रूप से mirror होती हैं।
  • Sticky हेडर हर scroll frame पर नेटिव रूप से pin किए जाते हैं।