تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

If / Each

If وEach يحددان أي العناصر موجودة: المحتوى الشرطي والمتكرر. لا ينشئان أي عنصر عرض أصلي (native view) خاص بهما. يحدّث العارض الأصلي ما تغيّر فقط: If يبدّل الفرع عند تغيّر حالة الشرط، وEach يضيف الصفوف وينقلها ويزيلها بحسب المفتاح بدلاً من إعادة بناء القائمة. في Rust توجد when(..) وeach(..) وdynamic(..) في nsh_core::prelude؛ وفي TSX توجد <If> و<Each>، وهما متغيرات عامة لا تحتاج إلى استيراد.

el-control-flow.tsx
el-control-flow.tsxLive
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, focusRing, hoverFill, Inspector, Panel, size, Stage, ui, wide } from "./palette";

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

let nextId = 4;

function App() {
  const todos = signal<Todo[]>([
    { id: 1, title: "Read the elements page", done: true },
    { id: 2, title: "Try <If> and <Each>", done: false },
    { id: 3, title: "Ship the app", done: false },
  ]);
  const showDone = signal(true);
  const visible = () => (showDone() ? todos() : todos().filter((t) => !t.done));
  const toggle = (id: number) => todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  const add = () => todos.update((list) => [...list, { id: nextId++, title: `Task ${nextId - 1}`, done: false }]);
  const remove = (id: number) => todos.update((list) => list.filter((t) => t.id !== id));
  const actions = () => (
    <View style={{ flexDirection: "row", gap: 8 }}>
      <View onPress={add} {...button("accent")}>
        <Text style={buttonText("accent")}>Add</Text>
      </View>
      <View onPress={() => showDone.set(!showDone())} {...button()}>
        <Text style={buttonText()}>{showDone() ? "Hide done" : "Show done"}</Text>
      </View>
    </View>
  );
  const list = () => (
    <If
      when={visible().length > 0}
      else={<Text style={{ color: ui().muted, textAlign: "center", marginTop: 40 }}>Nothing to show.</Text>}
    >
      <View style={{ gap: wide() ? 6 : 8 }}>
        <Each of={visible()} key={(t: Todo) => t.id}>
          {(todo, index) => (
            <View
              style={{
                ...ui().row,
                flexDirection: "row",
                alignItems: "center",
                gap: 12,
                ...(wide() ? { borderRadius: 8, height: size.row, paddingHorizontal: 12 } : { borderRadius: 12, padding: 12 }),
              }}
            >
              <View
                onPress={() => toggle(todo().id)}
                hitSlop={8}
                style={{
                  width: wide() ? 18 : 22,
                  height: wide() ? 18 : 22,
                  borderRadius: wide() ? 9 : 11,
                  borderWidth: 2,
                  borderColor: ui().accent,
                  backgroundColor: todo().done ? ui().accent : "transparent",
                }}
                hoverStyle={{ opacity: 0.88 }}
                focusStyle={focusRing()}
                activeStyle={{ opacity: 0.7 }}
              />
              <Text
                style={{
                  flex: 1,
                  fontSize: wide() ? size.label : undefined,
                  color: todo().done ? ui().faint : ui().text,
                  textDecorationLine: todo().done ? "line-through" : "none",
                }}
              >
                {index() + 1}. {todo().title}
              </Text>
              <If when={todo().done}>
                <View onPress={() => remove(todo().id)} hitSlop={8} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
                  <Text style={{ color: ui().down, fontSize: wide() ? 13 : undefined }}>Remove</Text>
                </View>
              </If>
            </View>
          )}
        </Each>
      </View>
    </If>
  );
  const counts = () => `${todos().filter((t) => t.done).length} of ${todos().length} done`;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>if / each</Text>
            <Text style={{ color: ui().muted }}>Click a circle to finish a task; Remove shows on finished ones</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={420}>
                {/* A fixed height, so the list grows downwards instead of moving. */}
                <View style={{ height: 360 }}>{list()}</View>
              </Stage>
              <Inspector>
                {actions()}
                <Panel title="todos · showDone">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{counts()}</Text>
                  <Text style={{ fontSize: 13, color: ui().text }}>
                    {showDone() ? "every task shown" : `${visible().length} not done shown`}
                  </Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 560, alignSelf: "center", padding: 20, paddingTop: 24, gap: 12 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>if / each</Text>
            {actions()}
            {list()}
            <Text style={{ fontSize: 12, color: ui().muted, fontVariant: ["tabular-nums"] }}>{counts()}</Text>
          </View>
        )
      }
    </View>
  );
}

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

view()
    .child(
        when(move || user.with(|u| u.is_some()), move || {
            text(move || format!("Hello, {}", user.with(|u| u.as_ref().map(|u| u.name.clone()).unwrap_or_default())))
        })
        .otherwise(|| text("Signed out")),
    )
    .child(each(todos, |t: &Todo| t.id, |todo, _index| text(move || todo.get().title)))

في TSX هما عامّان (أي ارتباط بالاسم نفسه في النطاق يحل محلهما). داخل تعابير JSX ما زال بإمكانك استخدام JavaScript العادي (cond ? a : b، items.map(…))، وفي Rust dynamic(move || ..): هذه تعيد إنشاء عناصرها في كل مرة يتغيّر فيها ما تقرؤه، بينما If وEach (when وeach في Rust) تحتفظ بالعناصر التي تبقى.

#If

يعرض فرعه ما دام الشرط متحققاً، وإلا يعرض الفرع الآخر (أو لا شيء).

الخاصيةRustالنوعالوصف
whenwhen(cond, ..)Tالشرط: قيمة أو signal أو closure في Rust؛ وفي TSX تُقيَّم قيمته من حيث الصدق (truthiness).
else.otherwise(|| ..)JSX.Elementيُعرض ما دام when غير متحقق.
childrenwhen(.., || ..)JSX.Element | (value: () => NonNullable<T>) => JSX.Elementيُعرض ما دام when متحققاً. في TSX تتلقى الدالة accessor لقيمة when.

يُنشأ الفرع عندما يتحول الشرط إليه، ويُزال ما أنشأه عندما يتحول الشرط عنه. أما when في TSX الذي ينتقل من قيمة صادقة إلى أخرى (مثل user() حين يتغير من كائن إلى آخر) فيحتفظ بالفرع، وتتحدّث التعابير داخله من تلقاء نفسها.

let loading = signal(true);

when(move || !loading.get(), || profile())
    .otherwise(|| text("Loading…").text_color(rgb(0x6e7781)))

في TSX تقرأ الدالة الابن القيمة التي أعطاها when، دون null أو undefined في نوعها، ولذلك لا يحتاج الفرع إلى !. وفي Rust يقرأ الفرع الـ signal نفسه، أو يحوّل dynamic القيمة إلى عناصر:

let user = signal(None::<User>);

dynamic(move || user.get().map(|u| text(u.name)))

#Each

يعرض صفاً لكل عنصر في القائمة.

الخاصيةRustالنوعالوصف
ofeach(items, ..)readonly any[] | null | undefinedالعناصر: في Rust Vec أو signal له أو closure يعيده. null وundefined لا يعرضان شيئاً.
keyeach(.., |item| key, ..)(item: any) => unknownهوية العنصر. الافتراضي في TSX: العنصر نفسه (الكائنات بحسب الهوية، والقيم الأولية بحسب القيمة).
childreneach(.., .., |item, index| row)(item: () => any, index: () => number) => JSX.Elementيعرض صفاً واحداً.

تتلقى دالة الصف accessors وليس قيماً: استدعِ item() وindex() في TSX، وitem.get() وindex.get() في Rust (كلاهما ReadSignal). عند تغيّر العناصر، تُحفظ الصفوف التي ما زال مفتاحها موجوداً (وتُنقل إذا تغيّر موضعها)؛ ويتحدّث عنصرها وفهرسها، ولا يُعاد تشغيل سوى التعابير التي تقرؤهما. المفاتيح الجديدة تحصل على صفوف جديدة؛ والصفوف التي اختفى مفتاحها تُزال.

#[derive(Clone, PartialEq)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

let todos = signal(Vec::<Todo>::new());

each(todos, |t: &Todo| t.id, |todo, index| {
    view()
        .flex_row()
        .gap_2()
        .child(text(move || format!("{}.", index.get() + 1)))
        .child(
            text(move || todo.get().title)
                .style_when(move || todo.get().done, |s| s.line_through()),
        )
})

#اختيار المفتاح

  • العناصر التي تُستبدل بكائنات جديدة في كل تحديث (التحديثات غير القابلة للتعديل، { ...t, done: true }) تحتاج إلى key، وعادةً ما يكون معرّفاً (id). بدونه يصبح كل كائن جديد صفاً جديداً وتُنشأ عناصره من جديد. أما في Rust فيلزم دائماً مفتاح.
  • في TSX تعمل مصفوفات النصوص أو الأرقام دون مفتاح: تُقارن بحسب القيمة.
  • لكل صف مالكه (owner) الخاص: الـ effects وعمليات التنظيف (cleanups) المنشأة في صف تنتهي عند إزالته.

#Each أم list

Each ينشئ كل الصفوف. هذا مناسب لعشرات أو بضع مئات من العناصر، في view أو scroll view. للبيانات الطويلة أو غير المحدودة استخدم list: فهو ينشئ الصفوف القريبة من منطقة العرض ويعيد تدويرها.

راجع JSX والمكوّنات والتفاعلية لمعرفة كيفية تحديث التعابير.