Naar de inhoud
Docxcelerate

Nodes

Het nodemodel

Wat alle nodes gemeen hebben, waarin ze verschillen, en waar elk van hen is gedocumenteerd.

Een document is een boom van nodes. Elke node is drie dingen: een id, een soort, en een regel om content te produceren.

<Paragraph          // the kind
  id="greeting"     // the address
>
  Dear {state.name},  {/* the rule */}
</Paragraph>

Opgelost tegen jouw data wordt het pure JSON:

{ "id": "greeting", "kind": "paragraph", "mode": "static", "text": "Dear Adaeze Nkemelu," }

Geen opmaak, geen lay-out. Die horen bij de renderer.

De catalogus

Elk type heeft een eigen pagina, met elke optie die het aanneemt en een preview van elke manier waarop het geschreven kan worden.

Structuur

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

Section-referentie, met previews →

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" />
);

Table of contents-referentie, met previews →

Tekst

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.

Helpers: Paragraph, useSetPrompts, useSetPlaceholders

export const Greeting: Paragraph = () => {
  const [state] = useState((data: SampleData) => ({ name: data.memberName }));

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

Paragraph-referentie, met previews →

Media

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

Image-referentie, met previews →

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

Shape-referentie, met previews →

clipArt Named visual blocks — rules, marks, callouts — drawn by the renderer. Gepland

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.

Data

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

Graph-referentie, met previews →

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.

Helpers: 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>
  );
};

Table-referentie, met previews →

Ids zijn adressen

Het id van een node is hoe een generatie-endpoint die alinea aanwijst, en hoe twee buildartefacten in een diff naast elkaar komen te liggen. Een id hernoemen breekt wat er van buitenaf naar wijst, net zoals het hernoemen van een API-route dat doet.

Houd ze uniek binnen het document. Het kost niets en maakt logs leesbaar.

Waar de content vandaan komt

Elke node heeft ofwel zijn content ofwel prompts om die te produceren, en dat beslist het component in plaats van dat het ergens gedeclareerd wordt. Geef een alinea tekst — of een Image een src, of een Graph zijn data — en hij lost op je eigen machine op. Geef hem in plaats daarvan prompts en een placeholder, dan wordt hij op het moment van de aanvraag ingevuld.

Beide lossen op naar dezelfde kind en verschillen in het gebouwde document door mode. mode is uitvoer, geen invoer: de build leidt het af uit wat het component meegaf, en het pakket vertelt een engine welke nodes die moet oplossen. Beide meegeven op één element is een fout, geen kop of munt.

Nodes schrijven behandelt de promptslots en wat previews in hun plaats laten zien.

Nesteling

section is vandaag de enige node die kinderen bevat, en hij accepteert elke soort, ook andere secties. Een tabelnode met nodes in zijn cellen is de volgende, volgens hetzelfde principe: containers bevatten de componenten die je toch al schrijft, in plaats van een tweede contentmodel ernaast.

Over deze previews

Elke preview hier is een echte build. src/nodes/ in de repository van deze site bevat één bestand per variant, geschreven tegen het gepubliceerde package; een buildstap lost elk daarvan op via buildDocument en rendert het met de renderer die dxcl dev serveert. De getoonde broncode is het bestand dat gedraaid heeft, en de JSON is wat eruit kwam — zodat deze pagina’s luid stukgaan in plaats van stilletjes te verouderen.


Deze pagina bewerken op GitHub ↗ Deze pagina als Markdown lezen