Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

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.

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

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

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

#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()),
        )
})

#Schlüssel wählen

  • Elemente, die bei jeder Aktualisierung durch neue Objekte ersetzt werden (unveränderliche Updates, { ...t, done: true }), benötigen ein key, 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.