Узлы
Модель узлов
Что общего у всех узлов, чем они различаются и где описан каждый из них.
Документ — это дерево узлов. Каждый узел есть три вещи: id, вид и правило, по которому получается содержимое.
<Paragraph // the kind
id="greeting" // the address
>
Dear {state.name}, {/* the rule */}
</Paragraph>
Разрешённый на ваших данных, он превращается в обычный JSON:
{ "id": "greeting", "kind": "paragraph", "mode": "static", "text": "Dear Adaeze Nkemelu," }
Ни стилей, ни вёрстки. Это дело рендерера.
Каталог
У каждого типа есть своя страница со всеми принимаемыми опциями и предпросмотром каждого способа записи.
Структура
Section Groups nodes under a titled heading.
The only construct that nests today. Its title carries into the document outline, so the structure you write is the structure the reader sees.
export const Opening: Section = () => (
<Section id="opening" title="Your renewal">
<Greeting />
<PriceChange />
</Section>
); TableOfContents A marker for a contents list, ahead of the renderers that build one.
The kind is part of the document schema and both renderers accept it. What is missing is downstream rather than in the authoring: the element places the marker and carries its title, and building the entries from the surrounding section titles is a renderer's job.
export const Contents: TableOfContents = () => (
<TableOfContents id="contents" title="What is in this letter" />
); Текст
Paragraph A block of prose, written from your data or generated from prompts.
The workhorse. A static paragraph holds the text as its children; a dynamic one carries prompts and a placeholder, and is filled at request time. Both land as the same node kind, differing only by `mode` — which is inferred from what you supply, never declared. Supplying both text and a prompt on one element is an error rather than a precedence rule.
Хелперы: Paragraph, useSetPrompts, useSetPlaceholders
export const Greeting: Paragraph = () => {
const [state] = useState((data: SampleData) => ({ name: data.memberName }));
return <Paragraph id="greeting">Dear {state.name},</Paragraph>;
}; Медиа
Image A picture resolved from your data or described by a prompt.
A static image points at something you hold — a signature, a logo, a site photograph — with every field able to vary per recipient. A dynamic image describes what is wanted and leaves the endpoint to make it.
export const Signature: Image = () => {
const [state] = useState((data: SampleData) => ({
src: data.signatureUrl,
manager: data.managerName,
}));
return (
<Image
id="signature"
src={state.src}
alt={`Signed by ${state.manager}`}
width={180}
height={60}
/>
);
}; Shape A drawn rectangle with the document's own words on top of it.
Word draws a real rectangle and the paragraphs sit on its fill rather than beside it. What separates it from a paragraph with a background is the size: a shape is a box you decided the dimensions of and does not grow with its text, which is exactly what a banner needs — one that changed depth with its wording would be a different notice on every letter. When the box *should* grow with what is in it, a `Table` with one cell is the thing to reach for.
export const RenewalBanner: ShapeComponent = () => {
const { date } = useFormat("en-GB");
const [state] = useState((data: SampleData) => ({
plan: data.plan,
renewsOn: data.renewsOn,
}));
return (
<Shape id="renewal-banner" variant="bannerAttention" height={44}>
<Paragraph id="renewal-banner-line">
{state.plan} renews on {date(state.renewsOn)}
</Paragraph>
</Shape>
);
}; clipArt Named visual blocks — rules, marks, callouts — drawn by the renderer. В планах
Not built yet. Nothing is fetched: the node names a shape and the renderer draws it, so it stays sharp in the DOCX and ships no asset.
Данные
Graph A real Word chart, declared as data.
Charts are declared, never drawn: `graphType` fixes the form, `data` carries the payload. What lands in the `.docx` is a DrawingML chart part with every value cached in it — the chart Word builds from Insert → Chart — so a reader can select it, restyle it and open its numbers. Nothing is rasterised, which is why charts cost the package no drawing library and no native module.
export const VisitsByMonth: Graph = () => {
const [state] = useState((data: SampleData) => ({
centreName: data.centreName,
months: data.visitsByMonth.map((entry) => entry.month),
visits: data.visitsByMonth.map((entry) => entry.visits),
}));
return (
<Graph
id="visits-by-month"
title={`Visits to ${state.centreName}`}
graphType="bar"
valueAxisTitle="Visits"
data={{
categories: state.months,
series: [{ label: "Visits", values: state.visits }],
}}
caption={`Your visits to ${state.centreName}, last six months`}
/>
);
}; Table A grid of cells, with the columns declared once.
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
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>
);
}; Идентификаторы — это адреса
Идентификатор узла — то, чем эндпоинт генерации адресует именно этот абзац, и то, по чему два артефакта сборки выстраиваются друг против друга в диффе. Переименование ломает всё, что ссылается на узел извне, — ровно как переименование маршрута API.
Держите их уникальными в пределах документа. Это ничего не стоит и делает логи читаемыми.
Откуда берётся содержимое
У каждого узла есть либо его содержимое, либо промпты, чтобы его получить, и
решает это компонент, а не какое-то объявление. Дайте абзацу текст — или Image
его src, или Graph его data — и он разрешится на вашей машине. Дайте вместо
этого промпты и заполнитель — и он будет заполнен во время запроса.
Оба разрешаются в один и тот же kind и различаются в собранном документе
значением mode. mode — это выход, а не вход: сборка выводит его из того, что
задал компонент, а пакет сообщает движку, какие узлы тому предстоит разрешить.
Указать и то и другое на одном элементе — ошибка, а не подбрасывание монетки.
О слотах промптов и о том, что предпросмотр показывает на их месте, — как писать узлы.
Вложенность
section — сегодня единственный узел, содержащий
дочерние, и он принимает любой вид, включая другие разделы. Следующим будет узел
таблицы с узлами в ячейках, по тому же принципу: контейнеры содержат те
компоненты, которые вы и так пишете, а не вторую модель содержимого рядом с
ними.
Об этих предпросмотрах
Каждый предпросмотр здесь — настоящая сборка. В src/nodes/ репозитория этого
сайта лежит по файлу на вариант, написанному против опубликованного пакета; шаг
сборки разрешает каждый из них через buildDocument и отрисовывает тем же
рендерером, который отдаёт dxcl dev. Показанный исходник — это тот файл,
который выполнялся, а JSON — то, что вернулось. Поэтому такие страницы ломаются
громко, а не устаревают тихо.
Изменить эту страницу на GitHub ↗ Читать эту страницу в Markdown