요소
If / Each
If and Each decide which elements exist: conditional and repeated content. They make no native view of their own.
The native renderer updates only what changed: If swaps a branch when its condition flips, Each adds, moves and
removes rows by key instead of rebuilding the list. Rust has when(..), each(..) and dynamic(..) in
nsh_core::prelude; TSX has <If> and <Each>, globals that need no import.
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 they are globals (a binding of the same name in scope takes their place). Inside JSX expressions you can still
use plain JavaScript (cond ? a : b, items.map(…)), and in Rust dynamic(move || ..): those make their elements
again every time what they read changes, while If and Each (when and each in Rust) keep the elements that stay.
#If
Renders its branch while the condition holds, else the other branch (or nothing).
| Prop | Rust | Type | Description |
|---|---|---|---|
when | when(cond, ..) | T | The condition: a value, a signal or a closure in Rust; in TSX its truthiness. |
else | .otherwise(|| ..) | JSX.Element | Rendered while when does not hold. |
children | when(.., || ..) | JSX.Element | (value: () => NonNullable<T>) => JSX.Element | Rendered while when holds. In TSX, a function gets an accessor of when’s value. |
A branch is made when the condition flips to it, and what it made goes when it flips away. A TSX when that goes from
one truthy value to another (user() changing from one object to another) keeps the branch, and the expressions inside
it update on their own.
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, a function child reads the value when gave, without null or undefined in its type, so the branch needs no
!. In Rust, the branch reads the signal itself, or dynamic maps the value to elements:
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
Renders a row for each item of a list.
| Prop | Rust | Type | Description |
|---|---|---|---|
of | each(items, ..) | readonly any[] | null | undefined | The items: in Rust a Vec, a signal of one or a closure returning one. null and undefined render nothing. |
key | each(.., |item| key, ..) | (item: any) => unknown | The identity of an item. TSX’s default: the item itself (objects by identity, primitives by value). |
children | each(.., .., |item, index| row) | (item: () => any, index: () => number) => JSX.Element | Renders one row. |
The row function gets accessors, not values: call item() and index() in TSX, item.get() and index.get() in
Rust (both are ReadSignals). When the items change, rows whose key is still there are kept (and moved if their position
changed); their item and index update, and only the expressions reading them run again. New keys get new rows; rows
whose key is gone are removed.
#[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>;#Choosing a key
- Items replaced by new objects on every update (immutable updates,
{ ...t, done: true }) need akey, usually an id. Without it each new object is a new row and its elements are made again. Rust always takes a key. - In TSX, arrays of strings or numbers work without a key: they are compared by value.
- Each row has its own owner: effects and cleanups created in a row end when the row is removed.
#Each or a list
Each creates every row. That is right for tens or a few hundred items, in a view or a scroll view.
For long or unbounded data, use a list: it creates the rows near the viewport and recycles them.
See JSX and components and reactivity for how expressions update.