العناصر
If / Each
If وEach يحددان أي العناصر موجودة: المحتوى الشرطي والمتكرر. لا ينشئان أي عنصر عرض أصلي (native view) خاص بهما.
يحدّث العارض الأصلي ما تغيّر فقط: If يبدّل الفرع عند تغيّر حالة الشرط، وEach يضيف الصفوف وينقلها ويزيلها
بحسب المفتاح بدلاً من إعادة بناء القائمة. في Rust توجد when(..) وeach(..) وdynamic(..) في
nsh_core::prelude؛ وفي TSX توجد <If> و<Each>، وهما متغيرات عامة لا تحتاج إلى استيراد.
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)))<View>
<If when={user()} else={<Text>Signed out</Text>}>
{(u) => <Text>Hello, {u().name}</Text>}
</If>
<Each of={todos()}>{(todo) => <Text>{todo().title}</Text>}</Each>
</View>في TSX هما عامّان (أي ارتباط بالاسم نفسه في النطاق يحل محلهما). داخل تعابير JSX ما زال بإمكانك استخدام
JavaScript العادي (cond ? a : b، items.map(…))، وفي Rust dynamic(move || ..): هذه تعيد إنشاء عناصرها
في كل مرة يتغيّر فيها ما تقرؤه، بينما If وEach (when وeach في Rust) تحتفظ بالعناصر التي تبقى.
#If
يعرض فرعه ما دام الشرط متحققاً، وإلا يعرض الفرع الآخر (أو لا شيء).
| الخاصية | Rust | النوع | الوصف |
|---|---|---|---|
when | when(cond, ..) | T | الشرط: قيمة أو signal أو closure في Rust؛ وفي TSX تُقيَّم قيمته من حيث الصدق (truthiness). |
else | .otherwise(|| ..) | JSX.Element | يُعرض ما دام when غير متحقق. |
children | when(.., || ..) | 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)))const loading = signal(true);
<If when={!loading()} else={<Text style={{ color: "#6e7781" }}>Loading…</Text>}>
<Profile />
</If>;في TSX تقرأ الدالة الابن القيمة التي أعطاها when، دون null أو undefined في نوعها، ولذلك لا يحتاج الفرع إلى
!. وفي Rust يقرأ الفرع الـ signal نفسه، أو يحوّل dynamic القيمة إلى عناصر:
let user = signal(None::<User>);
dynamic(move || user.get().map(|u| text(u.name)))const user = signal<User | null>(null);
<If when={user()}>{(u) => <Text>{u().name}</Text>}</If>;#Each
يعرض صفاً لكل عنصر في القائمة.
| الخاصية | Rust | النوع | الوصف |
|---|---|---|---|
of | each(items, ..) | readonly any[] | null | undefined | العناصر: في Rust Vec أو signal له أو closure يعيده. null وundefined لا يعرضان شيئاً. |
key | each(.., |item| key, ..) | (item: any) => unknown | هوية العنصر. الافتراضي في TSX: العنصر نفسه (الكائنات بحسب الهوية، والقيم الأولية بحسب القيمة). |
children | each(.., .., |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()),
)
})interface Todo {
id: number;
title: string;
done: boolean;
}
const todos = signal<Todo[]>([]);
<Each of={todos()} key={(t: Todo) => t.id}>
{(todo, index) => (
<View style={{ flexDirection: "row", gap: 8 }}>
<Text>{index() + 1}.</Text>
<Text style={{ textDecorationLine: todo().done ? "line-through" : "none" }}>{todo().title}</Text>
</View>
)}
</Each>;#اختيار المفتاح
- العناصر التي تُستبدل بكائنات جديدة في كل تحديث (التحديثات غير القابلة للتعديل،
{ ...t, done: true }) تحتاج إلىkey، وعادةً ما يكون معرّفاً (id). بدونه يصبح كل كائن جديد صفاً جديداً وتُنشأ عناصره من جديد. أما في Rust فيلزم دائماً مفتاح. - في TSX تعمل مصفوفات النصوص أو الأرقام دون مفتاح: تُقارن بحسب القيمة.
- لكل صف مالكه (owner) الخاص: الـ effects وعمليات التنظيف (cleanups) المنشأة في صف تنتهي عند إزالته.
#Each أم list
Each ينشئ كل الصفوف. هذا مناسب لعشرات أو بضع مئات من العناصر، في view أو scroll view.
للبيانات الطويلة أو غير المحدودة استخدم list: فهو ينشئ الصفوف القريبة من منطقة العرض ويعيد تدويرها.
راجع JSX والمكوّنات والتفاعلية لمعرفة كيفية تحديث التعابير.