Éléments
If / Each
If et Each décident quels éléments existent : contenu conditionnel et contenu répété. Ils ne créent aucune vue native propre.
Le moteur de rendu natif ne met à jour que ce qui a changé : If remplace une branche quand sa condition bascule, Each ajoute, déplace et
supprime des lignes par clé au lieu de reconstruire la liste. Rust dispose de when(..), each(..) et dynamic(..) dans
nsh_core::prelude ; TSX dispose de <If> et <Each>, des globales qui ne nécessitent aucun 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>En TSX, ce sont des globales (une liaison du même nom dans la portée prend leur place). Dans les expressions JSX, vous pouvez toujours utiliser
du JavaScript ordinaire (cond ? a : b, items.map(…)), et en Rust dynamic(move || ..) : ceux-ci recréent leurs éléments
à chaque fois que ce qu’ils lisent change, tandis que If et Each (when et each en Rust) conservent les éléments qui restent.
#If
Rend sa branche tant que la condition est vérifiée, sinon l’autre branche (ou rien).
| Prop | Rust | Type | Description |
|---|---|---|---|
when | when(cond, ..) | T | La condition : une valeur, un signal ou une fermeture en Rust ; en TSX, sa valeur de vérité. |
else | .otherwise(|| ..) | JSX.Element | Rendu tant que la condition when n’est pas remplie. |
children | when(.., || ..) | JSX.Element | (value: () => NonNullable<T>) => JSX.Element | Rendu tant que la condition when est remplie. En TSX, une fonction reçoit un accesseur de la valeur de when. |
Une branche est créée quand la condition bascule vers elle, et ce qu’elle a créé disparaît quand la condition bascule dans l’autre sens. Un when TSX qui passe d’une
valeur vraie à une autre (user() passant d’un objet à un autre) conserve la branche, et les expressions qu’elle contient
se mettent à jour d’elles-mêmes.
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 enfant de type fonction lit la valeur fournie par when, sans null ni undefined dans son type, de sorte que la branche n’a pas besoin de
!. En Rust, la branche lit elle-même le signal, ou dynamic transforme la valeur en éléments :
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
Rend une ligne pour chaque élément d’une liste.
| Prop | Rust | Type | Description |
|---|---|---|---|
of | each(items, ..) | readonly any[] | null | undefined | Les éléments : en Rust, un Vec, un signal d’un tel type ou une fermeture en renvoyant un. null et undefined ne rendent rien. |
key | each(.., |item| key, ..) | (item: any) => unknown | L’identité d’un élément. Valeur par défaut en TSX : l’élément lui-même (objets par identité, primitives par valeur). |
children | each(.., .., |item, index| row) | (item: () => any, index: () => number) => JSX.Element | Rend une ligne. |
La fonction de ligne reçoit des accesseurs, pas des valeurs : appelez item() et index() en TSX, item.get() et index.get() en
Rust (ce sont tous deux des ReadSignal). Quand les éléments changent, les lignes dont la clé est toujours présente sont conservées (et déplacées si leur position
a changé) ; leur élément et leur indice sont mis à jour, et seules les expressions qui les lisent sont réexécutées. Les nouvelles clés obtiennent de nouvelles lignes ; les lignes
dont la clé a disparu sont supprimées.
#[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>;#Choisir une clé
- Les éléments remplacés par de nouveaux objets à chaque mise à jour (mises à jour immuables,
{ ...t, done: true }) nécessitent unkey, généralement un identifiant. Sans lui, chaque nouvel objet devient une nouvelle ligne et ses éléments sont recréés. Rust exige toujours une clé. - En TSX, les tableaux de chaînes ou de nombres fonctionnent sans clé : ils sont comparés par valeur.
- Chaque ligne a son propre propriétaire : les effets et les nettoyages créés dans une ligne prennent fin quand la ligne est supprimée.
#Each ou une liste
Each crée chaque ligne. C’est adapté pour quelques dizaines ou quelques centaines d’éléments, dans une vue ou une vue défilante.
Pour des données longues ou non bornées, utilisez une liste : elle crée les lignes proches de la zone visible et les recycle.
Voir JSX et composants et réactivité pour savoir comment les expressions sont mises à jour.