Elementos
If / Each
If y Each deciden qué elementos existen: contenido condicional y repetido. No crean ninguna vista nativa propia.
El renderizador nativo actualiza solo lo que cambió: If intercambia una rama cuando su condición cambia, Each añade, mueve y
elimina filas por clave en lugar de reconstruir la lista. Rust tiene when(..), each(..) y dynamic(..) en
nsh_core::prelude; TSX tiene <If> y <Each>, globales que no necesitan importación.
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>En TSX son globales (un binding con el mismo nombre en el ámbito ocupa su lugar). Dentro de expresiones JSX todavía puedes
usar JavaScript normal (cond ? a : b, items.map(…)), y en Rust dynamic(move || ..): estos vuelven a crear sus elementos
cada vez que cambia lo que leen, mientras que If y Each (when y each en Rust) conservan los elementos que permanecen.
#If
Renderiza su rama mientras se cumple la condición; en caso contrario, la otra rama (o nada).
| Prop | Rust | Tipo | Descripción |
|---|---|---|---|
when |
when(cond, ..) |
T |
La condición: un valor, una signal o un closure en Rust; en TSX, su valor de verdad (truthiness). |
else |
.otherwise(|| ..) |
JSX.Element |
Se renderiza mientras when no se cumple. |
children |
when(.., || ..) |
JSX.Element | (value: () => NonNullable<T>) => JSX.Element |
Se renderiza mientras when se cumple. En TSX, una función recibe un accessor del valor de when. |
Una rama se crea cuando la condición cambia hacia ella, y lo que creó se elimina cuando cambia en sentido contrario. Un when de TSX que pasa
de un valor truthy a otro (user() cambiando de un objeto a otro) conserva la rama, y las expresiones dentro de
ella se actualizan por sí solas.
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>;En TSX, un hijo de tipo función lee el valor que dio when, sin null ni undefined en su tipo, así que la rama no necesita
!. En Rust, la rama lee la signal directamente, o dynamic convierte el valor en elementos:
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
Renderiza una fila por cada elemento de una lista.
| Prop | Rust | Tipo | Descripción |
|---|---|---|---|
of |
each(items, ..) |
readonly any[] | null | undefined |
Los elementos: en Rust un Vec, una signal de uno o un closure que devuelve uno. null y undefined no renderizan nada. |
key |
each(.., |item| key, ..) |
(item: any) => unknown |
La identidad de un elemento. Valor por defecto en TSX: el propio elemento (objetos por identidad, primitivos por valor). |
children |
each(.., .., |item, index| row) |
(item: () => any, index: () => number) => JSX.Element |
Renderiza una fila. |
La función de fila recibe accessors, no valores: llama a item() y index() en TSX, item.get() y index.get() en
Rust (ambos son ReadSignals). Cuando los elementos cambian, las filas cuya clave sigue presente se conservan (y se mueven si su posición
cambió); su elemento e índice se actualizan, y solo vuelven a ejecutarse las expresiones que los leen. Las claves nuevas obtienen filas nuevas; las filas
cuya clave desapareció se eliminan.
#[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>;#Elegir una clave
- Los elementos reemplazados por objetos nuevos en cada actualización (actualizaciones inmutables,
{ ...t, done: true }) necesitan unkey, normalmente un id. Sin él, cada objeto nuevo es una fila nueva y sus elementos se crean de nuevo. Rust siempre requiere una clave. - En TSX, los arrays de strings o números funcionan sin clave: se comparan por valor.
- Cada fila tiene su propio owner: los effects y cleanups creados en una fila terminan cuando la fila se elimina.
#Each o una list
Each crea todas las filas. Eso es adecuado para decenas o unos pocos cientos de elementos, en una view o una scroll view.
Para datos largos o ilimitados, usa una list: crea las filas cercanas al viewport y las recicla.
Consulta JSX y componentes y reactividad para saber cómo se actualizan las expresiones.