# Table

> A grid of cells, with the columns declared once and rows that are ordinary nodes.

Source: https://docxcelerate.com/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.

- **Helpers:** `Table`, `Row`, `Cell`
- **Node kind:** `table`
- **Category:** Data
- **Resolves:** Static
- **Children:** `Row`s, and any `.map()` producing them. A `Row` holds `Cell`s.

| Option | Type | What it does |
| --- | --- | --- |
| `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` _required_ | `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. |

## Writing one

A table needs two things from you: the columns, and the rows. You declare the
columns once on the `<Table>`, then fill it with `<Row>`s of `<Cell>`s.

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

Each column takes a `width` — in millimetres, or `"auto"` to share out whatever
the fixed ones leave — and an optional `align`.

**Why declare columns on the table instead of per cell?** Because every row
shares them, so declaring them once is what makes the rows line up. It also means
a cell only has to say what is in it.

## Rows are ordinary nodes

This is the part worth knowing, because it means there is less to learn than you
might expect.

Rows and cells are nodes, exactly like a paragraph or a section. So everything
you already know works on them:

- `.map()` over your data produces a row per entry, as above.
- A condition drops a row, the same way it drops any other node.
- Ids name themselves, so you don't have to invent one per row.

None of that is special-cased for tables. It is also why a published invoice
carries a single loop for the engine to walk, rather than a table full of
exceptions.

## What a cell holds

Most cells hold a value, so text goes straight in:

```tsx
<Cell>{line.qty}</Cell>
```

When one line isn't enough — a description with a quieter note under it — give
the cell paragraphs instead, and they stay on separate lines:

```tsx
<Cell>
  <Paragraph>{line.desc}</Paragraph>
  <Paragraph variant="muted">{line.meta}</Paragraph>
</Cell>
```

## Header rows

Marking a row `header` draws it as a heading:

```tsx
<Row header>
  <Cell>Description</Cell>
  <Cell>Amount</Cell>
</Row>
```

Only the rows a table **starts** with repeat onto each new page. That matters if
you mark a totals row as a header to give it emphasis: it still gets drawn as a
heading, but it stays put at the bottom, under the figures it adds up. It won't
be lifted to the top of page two.

## Variants

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

**What it resolves to**

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

**What it resolves to**

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

## Notes

- An empty cell is still a cell. Word won't accept a truly empty one, so it packs
  as a blank box rather than a column that has gone missing.
- `span` runs a cell across several columns. `align` overrides its column's
  alignment for that one cell.
- If you set a `variant` on a cell, it wins over one on the row, which wins over
  one on the table. The more specific one is usually the one you meant.
- Both renderers draw a real table — a `<table>` on screen, and a Word table with
  your declared column widths in the `.docx`.
