Skip to content
Docxcelerate

Essentials

Documents and nodes

A document is a tree of components, each taking its data as state and returning the nodes it wants.

A document in Docxcelerate is two things: a template and the data type it expects.

The template is a tree of components, and each component returns nodes — a paragraph, a section, a table. When you build the template against some data, you get back a DocumentModel: plain JSON describing what the document says, with no styling and no layout in it. Turning that into a .docx or a preview page comes later, and is somebody else’s job.

If you have written a React or Vue component before, most of this will feel familiar. The parts that differ are called out as we go.

A component takes its data as state

Here is about the smallest useful component you can write:

import { Paragraph, useState } from "docxcelerate/template";
import type { DocumentData } from "../types.ts";

export const Greeting: Paragraph = () => {
  const [state] = useState((data: DocumentData) => ({
    name: data.residentName,
  }));

  return <Paragraph id="greeting">Dear {state.name},</Paragraph>;
};

useState is where your data enters the component — and it is the only place it can. Everything after that line reads from state.

Why route everything through one call? Because it puts everything the component depends on in one place. You can read the top of the file and know what data it needs, instead of hunting for it through the rest of the code.

The initializer is ordinary TypeScript, so you can do whatever you like inside it. There is no template language to learn: conditionals, formatting, imports and .map() all work the way you already expect.

One thing that might look odd: Paragraph is both the element and the component type. In TypeScript a value and a type can share a name, so writing const Greeting: Paragraph declares what this component returns. Try to return a <Section> from it and you get a compile error.

Data can also arrive as props

If a component is handed what it needs, it doesn’t have to go and fetch it:

export const Arrears: Paragraph<{ amount: number }> = ({ amount }) => {
  const { currency } = useFormat();

  return <Paragraph id="arrears">You owe {currency(amount)}.</Paragraph>;
};

Something still has to read the data first, so a props-only component sits under a parent that took it into state.

Which should you use? Props for components you want to reuse against different values. State for components that know which document they belong to.

Decisions are ordinary if

You do not need a special construct to make a document conditional. Write the if you would write anywhere else:

export const Balance: Paragraph = () => {
  const [state] = useState((data: DocumentData) => ({
    settled: data.balanceDue === 0,
  }));

  if (state.settled) {
    return <Paragraph id="settled">Nothing outstanding.</Paragraph>;
  }

  return <Paragraph id="arrears">A balance remains.</Paragraph>;
};

A ternary works too, and so does {condition && <Node />} inside your JSX.

Give each arm its own id, as above. The resolved document then records which one this recipient actually got — which is the difference between a document you can audit later and one you can only look at.

Something changes about branching when you publish a document to an engine, but you don’t need it yet. We cover it in what changes when you publish.

Ids

An id is how an engine addresses a particular node, and how two build artifacts line up when you diff them. Renaming one is a breaking change for anything pointing at it, much like renaming an API route.

You can usually leave ids out. A node without one is named after what it is — its heading, or the component that produced it — so <Greeting /> becomes greeting. That is what stops loops and branches from forcing you to invent names for nodes nobody refers to.

The one thing you cannot do is use an id twice. Two nodes claiming the same id is an error, reported with both positions, rather than a race the later one wins.

Sections nest, nodes don’t

Section is the only element that groups other nodes under a heading, and its title carries into the document outline:

<Section id="opening" title="Opening">
  <Greeting />
  <Offer />
</Section>

Table holds nodes too — rows, and cells inside them — but it doesn’t head anything. See Section for how deeply you can nest, and Table for grids.

Hooks

Hooks are how a component reaches the build going on around it:

HookWhat it gives you
useStateData, taken in once and kept
useSharedA value left for the components rendered after this one
useSetPromptsPrompts for the node this returns — what makes it dynamic
useSetPlaceholdersWhat previews show in place of generated content
usePlaceholderDataStand-in names, dates and figures for previews
useFormatLocale-aware currency, dates, lists and plurals
useAvailableTokensThe token budget this build allotted
useDeriverRuns a registered deriver now

There is one rule, and it is the same one React has: call every hook before the first await, and before any branch or return. Hooks are matched up by the order you call them in, so a hook reached only sometimes would attach itself to the wrong component.

If you get this wrong, you get an error that says so. It will not fail quietly.

Building the document

Once you have a template, building it is one call:

import { buildDocument } from "docxcelerate";

const doc = await buildDocument(documentTemplate, data);

What comes back is a DocumentModel — a schemaVersion, an id, a title and an array of nodes. There is no styling or layout in it. Those get applied later by whichever renderer you hand it to.

If you have a document project (one defined with defineDocumentProject), reach for buildProjectPreviewDocument instead. It applies the project’s style and resolves dynamic nodes to their placeholders:

import { buildProjectPreviewDocument } from "docxcelerate";

const doc = await buildProjectPreviewDocument(project);

This is exactly what the preview app calls, so what you build in code matches what you saw in the browser.


Edit this page on GitHub ↗ Read this page as Markdown