Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

É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.

el-control-flow.tsx
el-control-flow.tsxLive
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)))

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).

PropRustTypeDescription
whenwhen(cond, ..)TLa condition : une valeur, un signal ou une fermeture en Rust ; en TSX, sa valeur de vérité.
else.otherwise(|| ..)JSX.ElementRendu tant que la condition when n’est pas remplie.
childrenwhen(.., || ..)JSX.Element | (value: () => NonNullable<T>) => JSX.ElementRendu 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)))

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)))

#Each

Rend une ligne pour chaque élément d’une liste.

PropRustTypeDescription
ofeach(items, ..)readonly any[] | null | undefinedLes éléments : en Rust, un Vec, un signal d’un tel type ou une fermeture en renvoyant un. null et undefined ne rendent rien.
keyeach(.., |item| key, ..)(item: any) => unknownL’identité d’un élément. Valeur par défaut en TSX : l’élément lui-même (objets par identité, primitives par valeur).
childreneach(.., .., |item, index| row)(item: () => any, index: () => number) => JSX.ElementRend 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()),
        )
})

#Choisir une clé

  • Les éléments remplacés par de nouveaux objets à chaque mise à jour (mises à jour immuables, { ...t, done: true }) nécessitent un key, 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.