Elemente
If / Each
If und Each bestimmen, welche Elemente existieren: bedingte und wiederholte Inhalte. Sie erzeugen selbst keine native View.
Der native Renderer aktualisiert nur, was sich geändert hat: If tauscht einen Zweig aus, wenn seine Bedingung umschlägt, Each fügt Zeilen anhand des Schlüssels hinzu, verschiebt und entfernt sie, statt die Liste neu aufzubauen. Rust hat when(..), each(..) und dynamic(..) in
nsh_core::prelude; TSX hat <If> und <Each>, Globals, die keinen Import benötigen.
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>In TSX sind sie Globals (eine Bindung gleichen Namens im Scope hat Vorrang). Innerhalb von JSX-Ausdrücken kannst du weiterhin
normales JavaScript verwenden (cond ? a : b, items.map(…)), in Rust dynamic(move || ..): Diese erzeugen ihre Elemente
jedes Mal neu, wenn sich ändert, was sie lesen, während If und Each (when und each in Rust) die Elemente behalten, die bestehen bleiben.
#If
Rendert seinen Zweig, solange die Bedingung gilt, sonst den anderen Zweig (oder nichts).
| Prop | Rust | Typ | Beschreibung |
|---|---|---|---|
when |
when(cond, ..) |
T |
Die Bedingung: in Rust ein Wert, ein Signal oder eine Closure; in TSX ihr Truthiness-Wert. |
else |
.otherwise(|| ..) |
JSX.Element |
Wird gerendert, solange when nicht gilt. |
children |
when(.., || ..) |
JSX.Element | (value: () => NonNullable<T>) => JSX.Element |
Wird gerendert, solange when gilt. In TSX erhält eine Funktion einen Accessor auf den Wert von when. |
Ein Zweig wird erzeugt, wenn die Bedingung zu ihm umschlägt, und was er erzeugt hat, verschwindet, wenn sie von ihm weg umschlägt. Ein TSX-when, das von einem truthy Wert zu einem anderen wechselt (user() wechselt von einem Objekt zu einem anderen), behält den Zweig, und die Ausdrücke darin aktualisieren sich selbstständig.
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>;In TSX liest ein Funktions-Child den Wert, den when geliefert hat, ohne null oder undefined in seinem Typ, sodass der Zweig kein
! benötigt. In Rust liest der Zweig das Signal selbst, oder dynamic bildet den Wert auf Elemente ab:
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
Rendert für jedes Element einer Liste eine Zeile.
| Prop | Rust | Typ | Beschreibung |
|---|---|---|---|
of |
each(items, ..) |
readonly any[] | null | undefined |
Die Elemente: in Rust ein Vec, ein Signal eines solchen oder eine Closure, die eines zurückgibt. null und undefined rendern nichts. |
key |
each(.., |item| key, ..) |
(item: any) => unknown |
Die Identität eines Elements. Standard in TSX: das Element selbst (Objekte nach Identität, Primitive nach Wert). |
children |
each(.., .., |item, index| row) |
(item: () => any, index: () => number) => JSX.Element |
Rendert eine Zeile. |
Die Zeilenfunktion erhält Accessors, keine Werte: Rufe in TSX item() und index() auf, in
Rust item.get() und index.get() (beides sind ReadSignals). Ändern sich die Elemente, bleiben Zeilen, deren Schlüssel weiterhin vorhanden ist, erhalten (und werden verschoben, wenn sich ihre Position geändert hat); ihr Element und ihr Index werden aktualisiert, und nur die Ausdrücke, die sie lesen, laufen erneut. Neue Schlüssel erhalten neue Zeilen; Zeilen, deren Schlüssel verschwunden ist, werden entfernt.
#[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>;#Schlüssel wählen
- Elemente, die bei jeder Aktualisierung durch neue Objekte ersetzt werden (unveränderliche Updates,
{ ...t, done: true }), benötigen einkey, meist eine ID. Ohne Schlüssel ist jedes neue Objekt eine neue Zeile, und ihre Elemente werden neu erzeugt. Rust verlangt immer einen Schlüssel. - In TSX funktionieren Arrays aus Strings oder Zahlen ohne Schlüssel: Sie werden nach Wert verglichen.
- Jede Zeile hat ihren eigenen Owner: Effekte und Cleanups, die in einer Zeile erzeugt wurden, enden, wenn die Zeile entfernt wird.
#Each oder eine Liste
Each erzeugt jede Zeile. Das passt für einige Dutzend oder wenige hundert Elemente, in einer View oder einer Scroll View.
Für lange oder unbegrenzte Daten verwende eine Liste: Sie erzeugt die Zeilen in der Nähe des Viewports und recycelt sie.
Wie Ausdrücke aktualisiert werden, siehst du unter JSX und Komponenten und Reaktivität.