Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

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.

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

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

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

#Elegir una clave

  • Los elementos reemplazados por objetos nuevos en cada actualización (actualizaciones inmutables, { ...t, done: true }) necesitan un key, 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.