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.
Viewport: 960 × 340

- 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>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"repeat-list": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "stretch",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"id": "panel",
"layout": {
"direction": "column",
"padding": "{space.5}",
"gap": "{space.4}",
"align": "stretch"
},
"style": {
"background": "{color.surface}",
"radius": "{radius.lg}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.lg}"
},
"text": "Quests"
},
{
"type": "Repeat",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "stretch"
},
"items": {
"bind": "quests.open"
},
"as": "q",
"key": "id",
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"height": 52,
"padding": "{space.2}",
"gap": "{space.3}",
"align": "center"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}"
},
"children": [
{
"type": "Text",
"layout": {
"width": 18
},
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": {
"bind": "q.$index"
}
},
{
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"gap": 2
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": {
"bind": "q.name"
}
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": {
"bind": "q.area"
}
}
]
},
{
"type": "Button",
"layout": {
"width": 78,
"height": 32,
"justify": "center",
"align": "center"
},
"style": {
"background": "#00000000",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line-strong}",
"radius": "{radius.md}",
"color": "{color.text}"
},
"states": {
"hover": {
"style": {
"background": "{color.slot}"
}
},
"pressed": {
"style": {
"background": "{color.brand-soft}"
}
},
"focused": {
"style": {
"borderWidth": "{border.focus}",
"borderColor": "{color.brand}"
}
},
"disabled": {
"style": {
"opacity": 0.45
}
}
},
"transition": {
"duration": "{motion.fast}"
},
"onClick": "track",
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.xs}"
},
"text": "Track"
}
]
}
]
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.sm}"
},
"text": "No quests in your log"
}
]
}
]
}
]
}
}
}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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| items | string | — | Data path del array, p. ej. "shop.items". |
| as | string | "item" | El alias contra el que enlaza la plantilla. |
| keyPath | string | absent | Path relativo al item que nombra su identidad. Se llama keyPath porque key es de React. |
| empty | ReactNode | absent | Se ve mientras el array está vacío, ausente o no es un array. |
| children | node | (item) => node | absent | La 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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| items | { bind: string } | — | El array enlazado. Siempre un binding. |
| as | string | "item" | El alias contra el que enlaza la plantilla. |
| key | string | absent | Path relativo al item que nombra su identidad estable. Ausente = posicional. |
| children | ZNode[] | absent | children[0] = plantilla del item; children[1..] = estado vacío. |
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 layout —direction, 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 dedisplay: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:
| Path | A 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
Repeaten 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 conwrapy 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
onClickde 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 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.
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>.