Saltar al contenido
zabloo

Repeat

El nodo que convierte una fila en muchas. Escribes una sola plantilla, enlazas un array del que es dueño el juego, y el SDK construye una copia por elemento.

primitivedesde v1sin focus

Un Repeat te deja escribir una fila una vez y enseñarla tantas veces como diga el dato. Le das un data path a un array del que es dueño el juego y un único nodo de plantilla; el SDK construye una copia de esa plantilla por elemento y apunta los bindings de cada copia a su propio item. Piensa en un registro de misiones: el documento que se publica lleva una fila de misión, y las siete que ve el jugador son esa fila instanciada contra quests.open. Lo usas siempre que cuántas cosas aparecen sea asunto del juego y no de la pantalla.

repeat-list.viewIR v1
Viewport

Viewport: 960 × 340

A Quests panel with three rows built from one template and numbered 0, 1 and 2 by their position in the bound array — Missing caravan in Dustfall, Wolves at the gate in North Road, and Ledger audit in Guild hall — each with its own Track button.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Button, Column, List, Row, Text } from "@zabloo/react";

export default function RepeatList() {
  return (
    <Column
      // `align: "stretch"` on the ROOT is what hands the panel the view's own
      // width; centring it instead would pin the panel to its content and no
      // change of viewport could reach it (ZAB-153).
      layout={{ grow: 1, justify: "center", align: "stretch", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Column
        id="panel"
        layout={{ padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
        style={{
          background: "{color.surface}",
          radius: "{radius.lg}",
          borderWidth: "{border.hairline}",
          borderColor: "{color.line}",
        }}
      >
        <Text style={{ color: "{color.text}", fontSize: "{text.lg}" }}>Quests</Text>

        <List
          items="quests.open"
          as="q"
          keyPath="id"
          layout={{ gap: "{space.2}", align: "stretch" }}
          // A slot, not a condition: the IR has no expressions to evaluate, so
          // "nothing here yet" is authored as a node that is in layout only
          // while the bound array is empty.
          empty={
            <Text style={{ color: "{color.faint}", fontSize: "{text.sm}" }}>
              No quests in your log
            </Text>
          }
        >
          {(q) => (
            <Row
              layout={{ height: 52, padding: "{space.2}", gap: "{space.3}", align: "center" }}
              style={{ background: "{color.slot}", radius: "{radius.md}" }}
            >
              {/* The element's position — the data has no such field. */}
              <Text
                bind={q("$index")}
                layout={{ width: 18 }}
                style={{ color: "{color.faint}", fontSize: "{text.xs}" }}
              />
              <Column layout={{ grow: 1, gap: 2 }}>
                <Text bind={q("name")} style={{ color: "{color.text}", fontSize: "{text.sm}" }} />
                <Text bind={q("area")} style={{ color: "{color.faint}", fontSize: "{text.xs}" }} />
              </Column>
              {/* One action for the whole list. Which row it came from travels
                  in the action context, not in a different action name. */}
              <Button
                variant="secondary"
                onClick="track"
                layout={{ width: 78, height: 32, justify: "center", align: "center" }}
              >
                <Text style={{ color: "{color.text}", fontSize: "{text.xs}" }}>Track</Text>
              </Button>
            </Row>
          )}
        </List>
      </Column>
    </Column>
  );
}
Tres filas de una sola plantilla. Abre la pestaña de IR y cuenta lo que hay de verdad en el envelope: una fila y un hueco para el estado vacío, nada más. Luego mira los números 0, 1 y 2 de las filas dibujadas — aparecen en unos datos que no los contienen, porque el SDK sabe la posición de cada copia.

Es un tipo de nodo en vez de una prop de Container por la razón que ha zanjado todas las preguntas parecidas de este formato: el SDK despacha comportamiento por tipo, nunca por tipo y prop.

Las dos tablas de abajo son donde las comodidades de autoría aterrizan como estructura. empty es una prop que escribes y un slot que se publica; keyPath se llama key en cuanto React sale de la foto y ya no hay ningún key propio con el que chocar.

Props de autoría

No existe un componente <Repeat>: los slots son posicionales, y <List> y <Grid> son el único sitio donde esa convención está escrita. Los dos comparten estas props.

PropTipoPor defectoDescripción
itemsstringData path del array, p. ej. "shop.items".
asstring"item"El alias contra el que enlaza la plantilla.
keyPathstringabsentPath relativo al item que nombra su identidad. Se llama keyPath porque key es de React.
emptyReactNodeabsentSe ve mientras el array está vacío, ausente o no es un array.
childrennode | (item) => nodeabsentLa plantilla del item — un solo nodo.

Además de estas, todo componente acepta las props base del nodo —id, visible, disabled, layout, style, states, transition, autofocus, clip— más variant, que el theme resuelve al exportar y que nunca aparece en la IR. <List> añade axis; <Grid> añade columns, itemWidth y cell.

Props de IR

Lo que llega al juego, cuando ya no queda capa de autoría.

PropTipoPor defectoDescripción
items{ bind: string }El array enlazado. Siempre un binding.
asstring"item"El alias contra el que enlaza la plantilla.
keystringabsentPath relativo al item que nombra su identidad estable. Ausente = posicional.
childrenZNode[]absentchildren[0] = plantilla del item; children[1..] = estado vacío.
items es siempre un binding

Un array literal aquí metería datos del juego dentro del documento, y el documento lleva estructura. Esa línea es lo que permite que el mismo envelope se publique una vez y lo rellene el estado propio de cada jugador.

El Repeat es el contenedor

Su propio layoutdirection, gap, padding, justify, align, wrap— coloca las instancias, exactamente igual que cualquier contenedor coloca a sus hijos. Eso es lo que permite que <List> y <Grid> sean azúcar de autoría sobre él en vez de tipos de nodo propios.

Los slots

  • children[0] es la plantilla, emitida una vez e instanciada por elemento.
  • children[1..] es el estado vacío, y solo está en el layout mientras el valor enlazado sea un array vacío, esté ausente o no sea un array — con la semántica de display:none, el único mecanismo de ocultar.

El estado vacío existe como slot porque enseñar Aquí no hay nada todavía necesitaría si no una expresión booleana sobre los datos, y la IR no tiene expresiones por decisión.

Enlazar dentro de la plantilla

Los paths de la plantilla pueden empezar por el alias y se resuelven contra el elemento actual:

PathA qué resuelve
"item"al elemento en sí — "shop.items.3"
"item.name"a "shop.items.3.name"
"item.$index"a la posición del elemento — un número que los datos no contienen
"player.gold"a sí mismo: un path que no cuelga de ningún alias conocido es absoluto

Los ámbitos se anidan y gana el alias más interior, que es la razón de que el alias se declare en vez de estar reservado: una lista anidada todavía puede llegar al elemento de fuera por su propio nombre. También significa que un alias tapa una raíz de datos que se llame igual — elige nombres que no sean raíces de tus datos.

La identidad

key nombra un path relativo al item que apunta a un campo estable ("id", "meta.sku"). La identidad es lo que hace estables las actualizaciones: reordenar un array con key mueve el estado por item del SDK —el focus, un Toggle marcado, un offset de scroll, una transition en marcha— con el item en vez de dejarlo clavado a una posición. Es también lo que hace posibles el reciclaje y la virtualización, y el focus va indexado por esa misma identidad, así que sobrevive a que una fila deje de estar realizada.

Sin key, la identidad es posicional.

Comportamiento

Estados

Solo disabled, y llega a todas las instancias: una prop apaga una lista entera de filas de golpe, sean las que sean las que acaben teniendo los datos.

Focusable

No, y nada de lo que haga el jugador llega al Repeat en sí. Las instancias conservan su propia focusabilidad; el nodo que las produjo no acepta input.

Layout

El suyo, aplicado a las instancias. Un <Grid> es este nodo con wrap y un ancho de celda: la geometría es aritmética, resuelta al escribir, porque la v1 no tiene dims fraccionarias.

Actions

Ninguna propia — pero el jugador pulsa un botón dentro de una fila → se dispara el onClick de ese botón → y el juego oye el nombre más un action context que nombra la fila de la que vino. Ese context es la capacidad entera que este nodo le añade a una action.

Degradación

En un SDK más antiguo

En un SDK más antiguo la fila se ve una vez, en blanco, con el mensaje de 'aquí no hay nada todavía' debajo.

Como Container: la plantilla se ve UNA vez, estática y sin resolver —sus bindings no leen nada— junto al estado vacío. El contenido sobrevive; la repetición no. Una lista de diez filas se queda en una fila en blanco, que se lee como un fallback y no como un crash.

Nothing here yet

Cómo oye esto el juego

Un solo nombre para todas las filas; cuál era viaja con él. Un único onClick: "track" en la plantilla se convierte en un botón por misión, y la action llega con un context —{ path, key, index }— que dice desde dónde se disparó. Eso es lo que permite que una lista de diez misiones necesite un nombre de action en vez de diez, y lo que mantiene a la UI sin saber nada de misiones.

El array en sí se mueve con SetData: escribir en quests.open vuelve a instanciar la plantilla, y escribir dentro de un item mueve los bindings de esa fila.

using UnityEngine;
using Zabloo;

[RequireComponent(typeof(ZablooDocument))]
public sealed class QuestLog : MonoBehaviour
{
  ZablooDocument _doc;

  void Start()
  {
      _doc = GetComponent<ZablooDocument>();
      _doc.OnAction += OnZablooAction;

      // The rows are the game's own array. The document carries the template,
      // never the data — which is why this line is the whole "populate" step.
      _doc.SetData("quests.open", QuestBook.Open);
  }

  void OnDestroy()
  {
      if (_doc != null) _doc.OnAction -= OnZablooAction;
  }

  void OnZablooAction(string action)
  {
      // One action for every row. Which one it came from rides in the context.
      if (action == "track") TrackSelectedQuest();
  }
}

Cuando la action se dispara desde dentro de un item de Repeat lleva un action context —el path, la key y el índice del item— para que el juego sepa qué fila se ha pulsado. El context es parte del formato y el renderer del navegador lo entrega; el OnAction de Unity es hoy Action<string> y entrega solo el nombre, así que un juego de Unity que necesite la fila la lee de su propio estado hasta que eso aterrice.

Composición

Repetir es una capacidad y hacer scroll es otra, así que una lista larga es una <List> dentro de un <ScrollView>.

  • <List> — para filas apiladas en una dirección: un registro de misiones, una bandeja de entrada, el stock de una tienda.
  • <Grid> — cuando las celdas son de un ancho conocido y deben envolver: un inventario, un selector de nivel, una lista de personajes.
  • empty — rellénalo siempre, porque “el array está vacío” es un estado al que el jugador va a llegar y no hay ninguna expresión que pueda taparte.
// List: the vertical case. The render-prop form gives you the item's paths
// as a function — it("name") → "it.name" — so renaming `as` follows through.
<List items="shop.items" as="it" keyPath="id" layout={{ gap: 8 }}
    empty={<Text>Nothing here yet</Text>}>
{(it) => (
  <Row layout={{ gap: 12, align: "center" }}>
    <Text bind={it("name")} />
    <Text bind={it("price")} />
    <Button onClick="buy"><Text>Buy</Text></Button>
  </Row>
)}
</List>

// Grid: the same Repeat, laid out as a wrapping row of sized cells.
// `columns` never reaches the IR — it is arithmetic done at authoring time,
// which is also why gap and padding must be numbers here, not tokens.
<Grid items="inventory.slots" columns={4} itemWidth={72} layout={{ gap: 8 }}>
{(slot) => <Text bind={slot("name")} />}
</Grid>

La plantilla es un único nodo porque children[0] es la plantilla: envuelve el contenido de un item en un <Row> o en un <Column>.

Relacionado