Naar de inhoud
Docxcelerate

Basis

Templates

Stel een documentboom samen met TSX, en bereken structuur met dezelfde JavaScript die je overal schrijft.

Een template benoemt een documentboom zodat een project ernaar kan wijzen. Niets ervan wordt gerenderd wanneer het geschreven wordt — het evalueren van de JSX bouwt alleen elementen, en dat is wat een component toelaat later te beslissen wat hij is, met data in de hand.

De vorm

import { Document, Section, template } from "docxcelerate/template";
import { Greeting, NextSteps } from "./nodes/index.ts";
import type { DocumentData } from "./types.ts";

export const documentTemplate = template<DocumentData>(
  <Document id="tenancy-renewal" title="Tenancy Renewal">
    <Section id="opening" title="Opening">
      <Greeting />
    </Section>
    <Section id="closing" title="Closing">
      <NextSteps />
    </Section>
  </Document>,
);

Wat die JSX aan Docxcelerate koppelt in plaats van aan React, is tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "docxcelerate/template"
  }
}

In aangemaakte workspaces staat dit al, dus bovenaan een bestand hoeft niets bij. Voeg je Docxcelerate toe aan een project dat jsxImportSource ergens anders naar laat wijzen, dan overschrijft een commentaar /** @jsxImportSource docxcelerate/template */ op regel één dat voor dat bestand.

Berekende structuur

Een template staat op modulescope, waar nog geen data bestaat, dus structuur die van data afhangt hoort in een component en niet in het template:

export const Enclosures: Section = () => {
  const [state] = useState((data: DocumentData) => ({ items: data.enclosures }));

  return (
    <Section id="enclosures" title="Enclosed">
      {state.items.map((item) => <Paragraph>{item.label}</Paragraph>)}
    </Section>
  );
};

Alles wat falsy is bij de children wordt overgeslagen, dus {condition && <Node />} leest zoals overal elders. Nodes zonder id krijgen er een van waar ze staan, en dat weerhoudt een map ervan namen te eisen die je niet hebt.

Dezelfde .map() werkt als de lijst pas per verzoek bekend is. Hier loopt hij door de collectie, en bij publiceren bereikt hij de engine als lus — zie lussen worden als lussen gepubliceerd hieronder.

Herbruikbare onderdelen

Een component is een gewone functie, dus een omhulsel in huisstijl ook. Componenten accepteren children net als al het andere:

export const Letterhead: Section<{ children?: Yield }> = ({ children }) => (
  <Section id="letterhead" title="">
    <Image id="logo" src="assets/logo.png" alt="Ashcroft Housing" />
    {children}
  </Section>
);

Een gedeelde hook is de andere helft hiervan. Doordat useSetPrompts prompts zet op wat de aanroepende component oplevert, kan huisstijl worden toegepast op een node die de hook niet eens bezit:

export function useHouseVoice() {
  useSetPrompts({
    systemPrompt: "You write for a housing association. Plain English, second person.",
  });
}

Elke component die useHouseVoice() aanroept, krijgt die systeemprompt op de node die hij teruggeeft, en kan hem nog steeds met een prop overschrijven.

Documentprojecten

Een documentproject bindt een template via één ingang, document.project.ts, aan zijn stijl en previewdata:

import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { documentStyle } from "./document-style.ts";
import type { DocumentData } from "./types.ts";

export default defineDocumentProject<DocumentData>({
  id: "tenancy-renewal",
  name: "Tenancy Renewal",
  version: "1.0.0",
  template: documentTemplate,
  style: documentStyle,
  previewData: {
    residentName: "Avery Mitchell",
    propertyRef: "Flat 4, Ashcroft House",
  },
});

previewData is waartegen de preview-app oplost. Houd het realistisch — korte namen en verzonnen plaatsnamen verbergen lay-outproblemen die echte data juist blootleggen.

Wat er verandert bij publiceren

Alles hierboven gaat ervan uit dat jij de data hebt. Wanneer je bouwt met data in handen — een preview, een lokale pack, een document dat je ter plekke schrijft — betekent een if precies wat er staat, en loopt een .map() door de lijst die hij kreeg.

Publiceren naar een engine is iets anders. Het artefact wordt één keer gebouwd, tegen vervangende waarden voor een aanvraag die nog niemand heeft gedaan, en opgeslagen. Een beslissing die van aanvraaggegevens afhangt, kan tijdens die build niet worden genomen; die moet naar de engine reizen en per document worden gemaakt.

Daaruit volgen drie regels, en de build handhaaft alle drie in plaats van een verkeerd document bij een ontvanger te laten belanden.

Reken per document, niet per build

Een waarde interpoleren werkt bij publiceren: het wordt het token dat de engine vervangt.

<Paragraph id="greeting">Hello {state.name},</Paragraph>
// gepubliceerd als: "Hello {{data.name}},"

Erop rekenen niet, want de waarde is nog niet bekend. Een bedrag opmaken, een lijst optellen, een datum vergelijken — daar is een deriver voor nodig, die de engine per document uitvoert:

<Paragraph
  id="balance"
  derivers={[
    derive("currencyLabel", { output: "balanceLabel", inputs: [dataRef("balanceDue")] }),
  ]}
>
  Your balance is {"{{derived.balanceLabel}}"}.
</Paragraph>

useFormat en useDeriver rekenen in plaats daarvan tijdens de build, wat juist is zodra de waarde al bekend is. Grijp naar een deriver wanneer dat niet zo is.

Lussen worden als lussen gepubliceerd

Een vertakking heeft twee takken en beide kunnen worden gepubliceerd. Een lus heeft er zoveel als de aanvraag items heeft, en dat aantal weet niemand tot er een document geschreven wordt — dus wat er opgeslagen wordt, is de lus zelf. Wat je schrijft is de gewone .map():

const [visits] = useState((data: TenancyData) => data.visits);

return visits.map((visit) => <Paragraph id="visit">Visit: {visit.label}</Paragraph>);

Met echte data is dit gewoon de .map() uit de standaardbibliotheek: hij loopt meteen door de collectie, dus previews tonen de herhaling in plaats van een beschrijving ervan. Publiceren kan dat niet, dus onderschept de plaatsvervanger diezelfde aanroep, draait de body één keer tegen een plaatsvervanger voor één item, en bereikt de lus de engine ongeschonden.

Niemand schrijft hier een {{ctx…}}-token. Het item dat de body kreeg, kent het pad waar het voor staat, dus de verwijzing schrijft zichzelf — en de rondes worden op beide paden op positie benoemd, zodat een id hetzelfde betekent in een preview als in het document dat iemand ontvangt.

Alles wat eerst naar de items moet kijken voordat de body geschreven kan worden — .filter(), .sort(), .length, een for-lus — is bij het publiceren niet te beantwoorden. Dat verwijst naar een deriver, die draait waar de data staat.

Vertakkingen vermenigvuldigen

Een gepubliceerde vertakking slaat beide takken op, elk onder de conditie die hem selecteert. Vertakkingen nesten vermenigvuldigt wat een document meedraagt, dus er is een limiet — branchLimit, standaard 32 — en eroverheen gaan laat de build falen in plaats van een stilletjes enorm artefact te produceren. Til de beslissing op naar een deriver die één waarde oplevert, of verhoog de limiet als het document werkelijk zo voorwaardelijk is.


Deze pagina bewerken op GitHub ↗ Deze pagina als Markdown lezen