Перейти к содержимому
Docxcelerate

Узлы

Таблица

Сетка ячеек: колонки объявляются один раз, а строки — обычные узлы.

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.

Хелперы
Table, Row, Cell
Вид узла
table
Категория
Данные
Разрешается
Static
Дочерние узлы
`Row`s, and any `.map()` producing them. A `Row` holds `Cell`s.
Опция Тип Что делает
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 обязательно 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.

Как её написать

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>Месяц</Cell>
        <Cell>Посещения</Cell>
      </Row>
      {state.months.map((entry) => (
        <Row>
          <Cell>{entry.month}</Cell>
          <Cell>{number(entry.visits)}</Cell>
        </Row>
      ))}
    </Table>
  );
};

Колонки принадлежат таблице — в миллиметрах или "auto", чтобы разделить остаток после фиксированных, у каждой необязательный align. Они объявляются один раз, потому что их разделяют все строки: таблица, колонки которой не выровнены, — не таблица.

Всё остальное — обычные узлы. .map() создаёт строки, условие убирает одну, и каждая называет себя сама. Именно поэтому опубликованный счёт несёт один цикл, который проходит движок, а не таблицу, полную особых случаев.

Что содержит ячейка

Текст попадает в неё напрямую, поэтому <Cell>{line.qty}</Cell> — обычный случай и читается именно так. Дайте ячейке абзацы, когда одной строки мало — описание над приглушённой заметкой — и они останутся на отдельных строках.

Строки заголовка

header у строки рисует её как заголовок. Повторяются на каждой новой странице только те строки, с которых таблица начинается, поэтому итоговая строка с header рисуется как заголовок, но остаётся на месте — под цифрами, которые складывает.

Варианты

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>
  );
};
table · rows from data Open ↗
Во что это разрешается

Узел в том виде, в каком он попадает в DocumentModel: 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

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>
  );
};
table · a closing row Open ↗
Во что это разрешается

Узел в том виде, в каком он попадает в DocumentModel: 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"
            }
          ]
        }
      ]
    }
  ]
}

Примечания

  • Пустая ячейка — всё равно ячейка. Word не принимает пустых, поэтому она упаковывается как пустое поле, а не как пропавшая колонка.
  • span растягивает ячейку на несколько колонок; align переопределяет выравнивание колонки только для этой ячейки.
  • variant у ячейки важнее, чем у строки, а тот — важнее, чем у таблицы: более узкое утверждение сделано осознаннее.
  • Оба поставляемых рендерера рисуют настоящую таблицу: <table> на экране и таблицу Word с объявленными ширинами колонок в .docx.

Изменить эту страницу на GitHub ↗ Читать эту страницу в Markdown