# Tabelle

> Ein Raster aus Zellen, mit einmal deklarierten Spalten und Zeilen, die gewöhnliche Nodes sind.

Source: https://docxcelerate.com/de/docs/nodes/table/

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.

- **Helper:** `Table`, `Row`, `Cell`
- **Node-Art:** `table`
- **Kategorie:** Daten
- **Wird aufgelöst:** Static
- **Kinder:** `Row`s, and any `.map()` producing them. A `Row` holds `Cell`s.

| Option | Typ | Was sie bewirkt |
| --- | --- | --- |
| `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` _erforderlich_ | `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. |

## So schreibt man eine

```tsx
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>Monat</Cell>
        <Cell>Besuche</Cell>
      </Row>
      {state.months.map((entry) => (
        <Row>
          <Cell>{entry.month}</Cell>
          <Cell>{number(entry.visits)}</Cell>
        </Row>
      ))}
    </Table>
  );
};
```

**Die Spalten gehören der Tabelle** — in Millimetern oder `"auto"`, um aufzuteilen, was die festen übrig lassen, jede mit optionalem `align`. Sie werden einmal deklariert, weil jede Zeile sie teilt: eine Tabelle, deren Spalten nicht fluchten, ist keine Tabelle.

**Alles andere ist ein gewöhnlicher Node.** Ein `.map()` erzeugt Zeilen, eine Bedingung lässt eine weg, und jede benennt sich selbst. Genau das lässt eine veröffentlichte Rechnung eine Schleife tragen, die die Engine abläuft, statt einer Tabelle voller Sonderfälle.

## Was eine Zelle enthält

Text kommt direkt hinein, also ist `<Cell>{line.qty}</Cell>` der Normalfall und liest sich auch so. Gib einer Zelle Absätze, wenn eine Zeile nicht reicht — eine Beschreibung über einer leisen Notiz — und sie bleiben auf getrennten Zeilen.

## Kopfzeilen

`header` an einer Zeile zeichnet sie als Kopfzeile. Nur die Zeilen, mit denen eine Tabelle **beginnt**, wiederholen sich auf jeder neuen Seite — eine als `header` markierte Summenzeile wird also als solche gezeichnet, bleibt aber unter den Zahlen, die sie addiert.

## Varianten

### Rows from data

A header row, then one row per entry from a `.map()`.

Source: `src/nodes/table/basic.node.tsx`

```tsx
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>
  );
};
```

**Wozu es aufgelöst wird**

```json
{
  "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

A cell holding two paragraphs, and a row marked as a heading.

Source: `src/nodes/table/totals.node.tsx`

```tsx
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>
  );
};
```

**Wozu es aufgelöst wird**

```json
{
  "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"
            }
          ]
        }
      ]
    }
  ]
}
```

## Anmerkungen

- Eine leere Zelle ist immer noch eine Zelle. Word nimmt keine leere an, also wird sie als leeres Feld gepackt statt als fehlende Spalte.
- `span` zieht eine Zelle über mehrere Spalten; `align` überschreibt die Ausrichtung der Spalte für genau diese Zelle.
- Ein `variant` an einer Zelle sticht eines an ihrer Zeile, und das eines an der Tabelle — die engere Aussage ist die bewusstere.
- Beide mitgelieferten Renderer zeichnen eine echte Tabelle: eine `<table>` am Bildschirm und eine Word-Tabelle mit den deklarierten Spaltenbreiten in der `.docx`.
