Nodos
Tabla
Una cuadrícula de celdas, con las columnas declaradas una vez y filas que son nodos corrientes.
The columns belong to the table, because every row shares them — a table whose columns do not line up is not a table. Everything else is an ordinary node: a `.map()` produces rows, a condition drops one, and each names itself. That is what lets a published invoice carry one loop the engine walks rather than a table full of special cases.
- Helpers
- Table, Row, Cell
- Clase de nodo
- table
- Categoría
- Datos
- Se resuelve
- Static
- Hijos
- `Row`s, and any `.map()` producing them. A `Row` holds `Cell`s.
| Opción | Tipo | Qué hace |
|---|---|---|
id | string | Stable address for the node. Engines target it and build artifacts diff on it, so treat a rename as a breaking change. Optional: a node without one takes an id from where it sits, which is what keeps branches and loops from forcing you to invent names. Two nodes claiming one id is an error rather than a race. |
columns obligatorio | TableColumn[] | The columns, left to right. Each takes a width in millimetres or "auto" to share what the fixed ones leave, and an align of left, center or right. |
variant | string | A block style the theme looks up — "band", "badge", "panel". Names what the node is, never what it looks like: the appearance lives in the style's blocks, so a document restyles without a node changing. A name the theme has not heard of draws as an ordinary block rather than failing. |
derivers | DeriverInvocation[] | Values the engine computes before the node resolves, written to derived.* and readable from a template token. Built with derive(). These survive publishing and run per document — use them for anything computed from request data. useDeriver runs one during the build instead. |
Cómo se escribe
import { Cell, Row, Table, useFormat, useState } from "docxcelerate/template";
export const VisitLog: Table = () => {
const { number } = useFormat("en-GB");
const [state] = useState((data: MemberData) => ({ months: data.visitsByMonth }));
return (
<Table id="visit-log" columns={[{ width: "auto" }, { width: 28, align: "right" }]}>
<Row header>
<Cell>Mes</Cell>
<Cell>Visitas</Cell>
</Row>
{state.months.map((entry) => (
<Row>
<Cell>{entry.month}</Cell>
<Cell>{number(entry.visits)}</Cell>
</Row>
))}
</Table>
);
};
Las columnas pertenecen a la tabla, en milímetros o "auto" para repartir lo que dejan las fijas, cada una con un align opcional. Se declaran una sola vez porque todas las filas las comparten: una tabla cuyas columnas no se alinean no es una tabla.
Todo lo demás es un nodo corriente. Un .map() produce filas, una condición elimina alguna, y cada una se nombra sola. Eso es lo que permite que una factura publicada lleve un bucle que el motor recorre en lugar de una tabla llena de casos especiales.
Qué contiene una celda
El texto entra directamente, así que <Cell>{line.qty}</Cell> es el caso habitual y se lee como tal. Dale párrafos a una celda cuando una línea no baste — una descripción sobre una nota atenuada — y se mantendrán en líneas separadas.
Filas de cabecera
header en una fila la dibuja como cabecera. Solo las filas con las que empieza una tabla se repiten en cada página nueva, así que una fila de totales marcada como header se dibuja como tal pero se queda donde está, bajo las cifras que suma.
Variantes
Rows from data
src/nodes/table/basic.node.tsx A header row, then one row per entry from a `.map()`.
import { Cell, Row, Table, useFormat, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* The columns are declared once, on the table, because every row shares them.
* A row is an ordinary node, so a `.map()` produces one per entry and the
* table needs to know nothing about loops.
*/
export const VisitLog: Table = () => {
const { number } = useFormat("en-GB");
const [state] = useState((data: SampleData) => ({ months: data.visitsByMonth }));
return (
<Table id="visit-log" columns={[{ width: "auto" }, { width: 28, align: "right" }]}>
<Row header>
<Cell>Month</Cell>
<Cell>Visits</Cell>
</Row>
{state.months.map((entry) => (
<Row>
<Cell>{entry.month}</Cell>
<Cell>{number(entry.visits)}</Cell>
</Row>
))}
</Table>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "visit-log",
"kind": "table",
"columns": [
{
"width": "auto"
},
{
"width": 28,
"align": "right"
}
],
"children": [
{
"id": "table-row",
"kind": "tableRow",
"header": true,
"children": [
{
"id": "table-cell",
"kind": "tableCell",
"children": [
{
"id": "table-cell-text",
"kind": "paragraph",
"mode": "static",
"text": "Month"
}
]
},
{
"id": "table-cell-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-2-text",
"kind": "paragraph",
"mode": "static",
"text": "Visits"
}
]
}
]
},
{
"id": "table-row-0",
"kind": "tableRow",
"children": [
{
"id": "table-cell-0",
"kind": "tableCell",
"children": [
{
"id": "table-cell-0-text",
"kind": "paragraph",
"mode": "static",
"text": "Apr"
}
]
},
{
"id": "table-cell-0-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-0-2-text",
"kind": "paragraph",
"mode": "static",
"text": "11"
}
]
}
]
},
{
"id": "table-row-1",
"kind": "tableRow",
"children": [
{
"id": "table-cell-1",
"kind": "tableCell",
"children": [
{
"id": "table-cell-1-text",
"kind": "paragraph",
"mode": "static",
"text": "May"
}
]
},
{
"id": "table-cell-1-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-1-2-text",
"kind": "paragraph",
"mode": "static",
"text": "14"
}
]
}
]
},
{
"id": "table-row-2",
"kind": "tableRow",
"children": [
{
"id": "table-cell-2-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-2-2-text",
"kind": "paragraph",
"mode": "static",
"text": "Jun"
}
]
},
{
"id": "table-cell-2-3",
"kind": "tableCell",
"children": [
{
"id": "table-cell-2-3-text",
"kind": "paragraph",
"mode": "static",
"text": "9"
}
]
}
]
},
{
"id": "table-row-3",
"kind": "tableRow",
"children": [
{
"id": "table-cell-3",
"kind": "tableCell",
"children": [
{
"id": "table-cell-3-text",
"kind": "paragraph",
"mode": "static",
"text": "Jul"
}
]
},
{
"id": "table-cell-3-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-3-2-text",
"kind": "paragraph",
"mode": "static",
"text": "16"
}
]
}
]
},
{
"id": "table-row-4",
"kind": "tableRow",
"children": [
{
"id": "table-cell-4",
"kind": "tableCell",
"children": [
{
"id": "table-cell-4-text",
"kind": "paragraph",
"mode": "static",
"text": "Aug"
}
]
},
{
"id": "table-cell-4-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-4-2-text",
"kind": "paragraph",
"mode": "static",
"text": "18"
}
]
}
]
},
{
"id": "table-row-5",
"kind": "tableRow",
"children": [
{
"id": "table-cell-5",
"kind": "tableCell",
"children": [
{
"id": "table-cell-5-text",
"kind": "paragraph",
"mode": "static",
"text": "Sep"
}
]
},
{
"id": "table-cell-5-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-5-2-text",
"kind": "paragraph",
"mode": "static",
"text": "12"
}
]
}
]
}
]
} A closing row
src/nodes/table/totals.node.tsx A cell holding two paragraphs, and a row marked as a heading.
import { Cell, Paragraph, Row, Table, useFormat, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* A cell takes text directly, and paragraphs when one line is not enough. The
* closing row is marked `header` so it is drawn as one — it stays where it is,
* because only the rows a table opens with repeat onto a new page.
*/
export const PriceSummary: Table = () => {
const { currency } = useFormat("en-GB");
const [state] = useState((data: SampleData) => ({
plan: data.plan,
lastPrice: data.lastPrice,
newPrice: data.newPrice,
}));
return (
<Table id="price-summary" columns={[{ width: "auto" }, { width: 30, align: "right" }]}>
<Row>
<Cell>
<Paragraph>{state.plan}</Paragraph>
<Paragraph>Last year</Paragraph>
</Cell>
<Cell>{currency(state.lastPrice)}</Cell>
</Row>
<Row header>
<Cell>From renewal</Cell>
<Cell>{currency(state.newPrice)}</Cell>
</Row>
</Table>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "price-summary",
"kind": "table",
"columns": [
{
"width": "auto"
},
{
"width": 30,
"align": "right"
}
],
"children": [
{
"id": "table-row",
"kind": "tableRow",
"children": [
{
"id": "table-cell",
"kind": "tableCell",
"children": [
{
"id": "paragraph",
"kind": "paragraph",
"mode": "static",
"text": "Peak Anytime"
},
{
"id": "paragraph-2",
"kind": "paragraph",
"mode": "static",
"text": "Last year"
}
]
},
{
"id": "table-cell-2",
"kind": "tableCell",
"children": [
{
"id": "table-cell-2-text",
"kind": "paragraph",
"mode": "static",
"text": "£468.00"
}
]
}
]
},
{
"id": "table-row-2",
"kind": "tableRow",
"header": true,
"children": [
{
"id": "table-cell-3",
"kind": "tableCell",
"children": [
{
"id": "table-cell-3-text",
"kind": "paragraph",
"mode": "static",
"text": "From renewal"
}
]
},
{
"id": "table-cell-4",
"kind": "tableCell",
"children": [
{
"id": "table-cell-4-text",
"kind": "paragraph",
"mode": "static",
"text": "£492.00"
}
]
}
]
}
]
} Notas
- Una celda vacía sigue siendo una celda. Word no acepta ninguna vacía, así que se empaqueta como un recuadro en blanco y no como una columna que falta.
spanextiende una celda por varias columnas;alignanula la alineación de su columna solo para esa celda.- Un
varianten una celda gana al de su fila, que gana al de la tabla: la afirmación más concreta es la más deliberada. - Ambos renderizadores dibujan una tabla de verdad: una
<table>en pantalla y una tabla de Word con los anchos de columna declarados en el.docx.