Fundamentos
Templates
Compón un árbol de documento con TSX, y calcula la estructura con el mismo JavaScript que escribes en todas partes.
Una plantilla nombra un árbol de documento para que un proyecto pueda apuntar a él. Nada de lo que hay en ella se renderiza cuando se escribe — evaluar el JSX solo construye elementos, y eso es lo que permite a un componente decidir qué es más tarde, con los datos en la mano.
La forma
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>,
);
Lo que conecta ese JSX con Docxcelerate en lugar de con React es
tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "docxcelerate/template"
}
}
Los workspaces generados ya lo traen, así que no hay nada que añadir al principio
de cada archivo. Si estás incorporando Docxcelerate a un proyecto que apunta
jsxImportSource a otro sitio, un comentario
/** @jsxImportSource docxcelerate/template */ en la primera línea lo
sobrescribe para ese archivo.
Estructura calculada
Una plantilla se escribe en el ámbito del módulo, donde todavía no hay datos, así que la estructura que depende de los datos va en un componente, no en la plantilla:
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>
);
};
Cualquier hijo falsy se omite, así que {condition && <Node />} se lee como en
cualquier otro sitio. Los nodos sin id toman uno de donde están, y eso impide que
un map exija nombres que no tienes.
El mismo .map() sirve cuando la lista solo se conoce por petición. Aquí recorre
la colección, y al publicar llega al motor como un bucle — mira los bucles se
publican como bucles más abajo.
Piezas reutilizables
Un componente es una función corriente, así que una envoltura de estilo de la casa también lo es. Los componentes aceptan hijos como cualquier otra cosa:
export const Letterhead: Section<{ children?: Yield }> = ({ children }) => (
<Section id="letterhead" title="">
<Image id="logo" src="assets/logo.png" alt="Ashcroft Housing" />
{children}
</Section>
);
Un hook compartido es la otra mitad de esto. Como useSetPrompts fija prompts en
lo que produzca el componente que lo llama, el estilo de la casa puede aplicarse
a un nodo que el hook ni siquiera posee:
export function useHouseVoice() {
useSetPrompts({
systemPrompt: "You write for a housing association. Plain English, second person.",
});
}
Cualquier componente que llame a useHouseVoice() recibe ese prompt de sistema
en el nodo que devuelve, y aún puede sobrescribirlo con una prop.
Proyectos de documento
Un proyecto de documento ata una plantilla a su estilo y a sus datos de vista
previa mediante un único punto de entrada, document.project.ts:
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 es contra lo que resuelve la app de vista previa. Manténlo
realista — los nombres cortos y las ciudades de relleno ocultan problemas de
maquetación que los datos reales sí exponen.
Qué cambia al publicar
Todo lo anterior da por hecho que tienes los datos. Cuando compilas con los datos
en la mano — una vista previa, un empaquetado local, un documento escrito sobre la
marcha — un if significa exactamente lo que dice, y un .map() recorre la lista
que se le dio.
Publicar en un motor es distinto. El artefacto se compila una vez, contra valores de relleno para una petición que nadie ha hecho todavía, y se guarda. Una decisión que dependa de los datos de la petición no se puede zanjar durante esa compilación; tiene que viajar al motor y tomarse por documento.
De ahí salen tres reglas, y la compilación hace cumplir las tres en lugar de dejar que un documento equivocado llegue a quien lo recibe.
Calcula por documento, no por compilación
Interpolar un valor funciona al publicar: se convierte en el token que el motor sustituye.
<Paragraph id="greeting">Hello {state.name},</Paragraph>
// publicado como: "Hello {{data.name}},"
Calcular sobre él no, porque el valor todavía no se conoce. Formatear una moneda, sumar una lista, comparar una fecha — eso necesita un deriver, que el motor ejecuta por documento:
<Paragraph
id="balance"
derivers={[
derive("currencyLabel", { output: "balanceLabel", inputs: [dataRef("balanceDue")] }),
]}
>
Your balance is {"{{derived.balanceLabel}}"}.
</Paragraph>
useFormat y useDeriver calculan durante la compilación, que es lo correcto
siempre que el valor ya se conozca. Echa mano de un deriver cuando no.
Los bucles se publican como bucles
Una bifurcación tiene dos ramas y las dos se pueden publicar. Un bucle tiene
tantas como entradas traiga la petición, y ese número no lo sabe nadie hasta que
se escribe un documento — así que lo que se guarda es el bucle en sí. Lo que se
escribe es el .map() de siempre:
const [visits] = useState((data: TenancyData) => data.visits);
return visits.map((visit) => <Paragraph id="visit">Visit: {visit.label}</Paragraph>);
Con datos reales esto es el .map() de la biblioteca estándar: recorre la
colección de inmediato, así que las vistas previas enseñan la repetición en vez de
una descripción de ella. Al publicar no se puede recorrer, así que el sustituto
intercepta esa misma llamada, ejecuta el cuerpo una vez contra un sustituto de una
entrada, y el bucle llega intacto al motor.
Aquí nadie escribe un token {{ctx…}}. La entrada que recibió el cuerpo sabe la
ruta que representa, así que la referencia se escribe sola — y las pasadas se
nombran por posición en ambos caminos, de modo que un id significa lo mismo en una
vista previa y en el documento que recibe alguien.
Todo lo que tenga que mirar las entradas antes de poder escribir el cuerpo —
.filter(), .sort(), .length, un bucle for — no se puede responder al
publicar. Eso remite a un deriver, que se ejecuta donde están los datos.
Las bifurcaciones multiplican
Publicar una bifurcación guarda las dos ramas, cada una bajo la condición que la
selecciona. Anidar bifurcaciones multiplica lo que carga un documento, así que hay
un límite — branchLimit, 32 por defecto — y pasarlo hace fallar la compilación en
vez de producir un artefacto calladamente enorme. Sube la decisión a un deriver que
dé un solo valor, o eleva el límite si el documento de verdad es así de
condicional.