Bind data to a Word template through content controls (w:sdt): find a control by tag, set its text or checkbox, or replace a whole region with a generated document. The result stays a real .docx that round-trips.

Fill Word templates from code, by tag, in TypeScript or C#

Bind data to a Word template through content controls (w:sdt): find a control by tag, set its text or checkbox, or replace a whole region with a generated document. The result stays a real .docx that round-trips.

Every team that generates contracts or reports ends up filling a Word template. The template is a .docx a designer made, with slots for a name, a date, and a signature block. Fill those slots from code and you have a document. The usual ways to do it hurt:

  • Search-and-replace on {{placeholders}} breaks the moment a token lands across two runs or a user retypes half of it.
  • A mail-merge library flattens your careful template into generated markup that Word no longer treats as authored.

Word already ships the right primitive: the content control, a structured document tag (w:sdt). Your designer wraps each fillable region in one and tags it. You bind data to the tag from code. The filled file stays a real Word document with the controls in place, so the next person opens it, edits it, and re-runs your fill.

@forevka/wordcanvas reads those controls, writes them, and round-trips them back to Word, in TypeScript and in C#.

Find a control, set its value

Tag a control customer.name in the template. Fill it by tag:

import { DocumentEditor, getSdtsByTag } from "@forevka/wordcanvas/query";

const editor = new DocumentEditor(template);

for (const { id } of getSdtsByTag(editor.doc, "customer.name")) {
  editor.setSdtText(id, order.customer);
}

editor
  .setCheckbox(expeditedId, order.expedited)
  .setSdtValue(regionId, order.region);

setSdtText fills a text control and clears its gray placeholder. setCheckbox flips a checkbox. setSdtValue picks an option on a drop-down or combo box by its stored value. Each call returns the editor, so a fill routine reads as one chain, and the whole thing is one undo step.

Replace a whole region with a generated document

Some slots hold more than a string. A line-items section is a table whose rows depend on the order. Build that section with a DocumentBuilder, then drop it into the control:

import { DocumentBuilder } from "@forevka/wordcanvas/builder";

const lineItems = DocumentBuilder.create()
  .table(
    [["Item", "Qty", "Price"], ...order.lines.map((l) => [l.name, `${l.qty}`, l.price])],
    { headerRow: true },
  )
  .build();

editor.replaceSdtContent("line-items", lineItems);

replaceSdtContent swaps the entire content of a block-level control for another Document, reconciling every id space (styles, lists, bookmarks, nested controls) so nothing collides. The control keeps its tag and its spot in the template. Author the region however suits you, then splice it in.

Scrape values back out

The same controls read as well as they write. Pull a returned template apart into data:

import { getSdtValue } from "@forevka/wordcanvas/query";

const name = getSdtValue(doc, nameId)?.text;
const agreed = getSdtValue(doc, termsId)?.checked;
const region = getSdtValue(doc, regionId)?.selected;

getSdtValue returns the shape the control's type implies: text for any control, checked for a checkbox, selected for a drop-down. So you fill in one direction and read back in the other, off the same tagged template.

Nesting survives

A repeatable party block that holds its own name control and address control is a control inside a control. WordCanvas tracks the ancestry, so replaceSdtContent on the outer block keeps the inner controls, and getSdtChildren and getSdtAncestors walk the tree. Your template's structure returns the way the designer drew it.

The same code in C#

The C# bindings mirror the TypeScript surface one for one, so a .NET report service fills the same templates:

using var engine = new WordCanvasEngine();
WordDocument template = engine.ImportDocx(File.ReadAllBytes("contract-template.docx"));

WordDocument filled = template.Edit()
    .SetSdtText(nameId, order.Customer)
    .SetCheckbox(expeditedId, order.Expedited)
    .SetSdtValue(regionId, order.Region)
    .ToDocument()
    .ReplaceSdtContent("line-items", lineItems);

byte[] pdf = filled.ExportPdf();

Try it

npm install @forevka/wordcanvas

Tag one region in your template, then fill it:

import { DocumentEditor, getSdtsByTag } from "@forevka/wordcanvas/query";

const editor = new DocumentEditor(template);
for (const { id } of getSdtsByTag(editor.doc, "greeting")) {
  editor.setSdtText(id, `Hello, ${name}`);
}
const filled = editor.doc;

Ship the template your designer made, and bind your data to it by tag.