# Templates

> Собирайте дерево документа на TSX и вычисляйте структуру тем же JavaScript, который пишете везде.

Source: https://docxcelerate.com/ru/docs/essentials/templates/

Шаблон называет дерево документа, чтобы проект мог на него указать. Ничего из
него не отрисовывается в момент написания — вычисление JSX только строит
элементы, и именно это позволяет компоненту решить, чем он является, позже, уже
с данными на руках.

## Форма

```tsx
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>,
);
```

С Docxcelerate, а не с React, этот JSX связывает `tsconfig.json`:

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

В созданных workspace это уже прописано, так что вверху файла добавлять нечего.
Если вы подключаете Docxcelerate к проекту, где `jsxImportSource` указывает в
другое место, комментарий `/** @jsxImportSource docxcelerate/template */` в
первой строке переопределит его для этого файла.

## Вычисляемая структура

Шаблон пишется на уровне модуля, где данных ещё нет, поэтому структура,
зависящая от данных, живёт в компоненте, а не в шаблоне:

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

Всё ложное среди потомков пропускается, поэтому `{condition && <Node />}`
читается так же, как и везде. Узлы без id получают его из своего места, и это
избавляет `map` от требования имён, которых у вас нет.

Тот же `.map()` работает и когда список известен только на каждый запрос. Здесь он
обходит коллекцию, а при публикации доходит до движка как цикл: см. [циклы
публикуются как циклы](#циклы-публикуются-как-циклы) ниже.

## Переиспользуемые части

Компонент — обычная функция, значит и обёртка в фирменном стиле тоже. Компоненты
принимают потомков, как и всё остальное:

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

Общий хук — вторая половина этого. Поскольку `useSetPrompts` задаёт промпты на
том узле, который отдаёт вызывающий компонент, фирменный стиль можно применить к
узлу, который хуку вовсе не принадлежит:

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

Любой компонент, вызвавший `useHouseVoice()`, получает этот системный промпт на
возвращаемом узле — и всё ещё может переопределить его пропсом.

## Проекты документов

Проект документа связывает шаблон с его стилем и данными предпросмотра через
единственную точку входа, `document.project.ts`:

```tsx
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` разрешает приложение предпросмотра. Держите их реалистичными —
короткие имена и выдуманные города скрывают проблемы вёрстки, которые настоящие
данные как раз обнажают.

## Что меняется при публикации

Всё вышеописанное исходит из того, что данные у вас есть. Когда вы собираете с
данными на руках — предпросмотр, локальная упаковка, документ, написанный тут же,
— `if` означает ровно то, что написано, а `.map()` проходит по тому списку,
который ему дали.

Публикация в [движок](/ru/docs/generation/endpoint/) устроена иначе. Артефакт
собирается **один раз**, на подставных значениях для запроса, которого ещё никто
не сделал, и сохраняется. Решение, зависящее от данных запроса, на этой сборке
принять нельзя: оно должно доехать до движка и приниматься на каждый документ.

Отсюда следуют три правила, и сборка обеспечивает все три, вместо того чтобы
позволить неверному документу дойти до получателя.

### Считайте на документ, а не на сборку

Подстановка значения при публикации работает: она становится токеном, который
подставит движок.

```tsx
<Paragraph id="greeting">Hello {state.name},</Paragraph>
// опубликовано как: "Hello {{data.name}},"
```

А вычисления над ним — нет, потому что значение ещё неизвестно. Отформатировать
сумму, сложить список, сравнить дату — для этого нужен **деривер**, который
движок выполняет на каждый документ:

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

`useFormat` и `useDeriver` считают во время сборки, и это правильно всегда, когда
значение уже известно. За деривером тянитесь тогда, когда оно неизвестно.

### Циклы публикуются как циклы

У ветвления две ветки, и обе можно опубликовать. У цикла их столько, сколько
записей в запросе, и это число никому не известно, пока документ не пишется, — так
что сохраняется сам цикл. Пишется при этом обычный `.map()`:

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

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

С настоящими данными это просто `.map()` из стандартной библиотеки: он сразу
обходит коллекцию, поэтому предпросмотры показывают повторение, а не его описание.
При публикации обойти нечего, поэтому заглушка перехватывает тот же вызов,
прогоняет тело один раз против заглушки для одной записи, и цикл доходит до движка
нетронутым.

Токен `{{ctx…}}` здесь никто не пишет. Запись, которую получило тело, знает путь,
за который она стоит, так что ссылка пишется сама — а проходы на обоих путях
именуются по позиции, поэтому id значит одно и то же и в предпросмотре, и в
документе, который получит адресат.

Всё, что должно сперва посмотреть на записи, прежде чем тело можно написать —
`.filter()`, `.sort()`, `.length`, цикл `for`, — при публикации ответить нельзя.
Это отсылает к дериверу, который выполняется там, где лежат данные.

### Ветвления умножаются

Публикация ветвления сохраняет обе ветки, каждую под условием, которое её
выбирает. Вложенные ветвления умножают то, что несёт в себе документ, поэтому есть
предел — `branchLimit`, по умолчанию 32, — и его превышение роняет сборку, вместо
того чтобы тихо породить огромный артефакт. Поднимите решение в деривер, дающий
одно значение, или поднимите предел, если документ действительно настолько
условный.
