Fill a DOCX template
Update every content-control occurrence that represents an application field, then export the DOCX.
Treat your application data as the source of truth. Find controls by tag, update every returned target, and inspect each mutation receipt before reporting success.
Try the workflow
Change the client name. All three occurrences update. Toggle auto-renew to change the Word checkbox stored in the DOCX.
Define the field map
Keep business-field keys in application code. A tag connects one field to one or more controls in the document:
export const templateFields = [
{
key: 'clientLegalName',
label: 'Client legal name',
tag: 'client.legalName',
type: 'text',
},
{
key: 'clientAddress',
label: 'Client address',
tag: 'client.address',
type: 'text',
},
{
key: 'effectiveDate',
label: 'Effective date',
tag: 'agreement.effectiveDate',
type: 'text',
},
{
key: 'autoRenew',
label: 'Auto-renew',
tag: 'agreement.autoRenew',
type: 'checkbox',
},
] as const;
export type TemplateField = (typeof templateFields)[number];
export type TemplateFieldKey = TemplateField['key'];
type TaggedContentControl = {
readonly controlType: string;
readonly properties: { readonly tag?: string };
};
export function hasCompatibleTemplateFields(items: readonly TaggedContentControl[]) {
return templateFields.every((field) =>
items.some((item) => item.properties.tag === field.tag && item.controlType === field.type),
);
}
Update every occurrence
selectByTag() returns each matching control. Update them one at a time so the result can distinguish complete,
partial, and failed updates:
import type { BrowserDocumentApi, ContentControlInfo } from 'superdoc/ui';
export type FieldUpdateResult = {
failures: string[];
matched: number;
unchanged: number;
updated: number;
};
type ControlType = 'checkbox' | 'text';
async function updateControls(
doc: BrowserDocumentApi,
tag: string,
expectedType: ControlType,
mutate: (control: ContentControlInfo) => Promise<{ success: boolean; failure?: { code?: string; message?: string } }>,
): Promise<FieldUpdateResult> {
let items: readonly ContentControlInfo[];
try {
({ items } = await doc.contentControls.selectByTag({ tag }));
} catch (error) {
return {
failures: [error instanceof Error ? error.message : `Could not find controls for ${tag}.`],
matched: 0,
unchanged: 0,
updated: 0,
};
}
const result: FieldUpdateResult = { failures: [], matched: items.length, unchanged: 0, updated: 0 };
for (const control of items) {
if (control.controlType !== expectedType) {
result.failures.push(`${control.id} is ${control.controlType}, not ${expectedType}.`);
continue;
}
try {
const receipt = await mutate(control);
if (receipt.success) result.updated += 1;
else if (receipt.failure?.code === 'NO_OP') result.unchanged += 1;
else result.failures.push(receipt.failure?.message ?? `Could not update ${control.id}.`);
} catch (error) {
result.failures.push(error instanceof Error ? error.message : `Could not update ${control.id}.`);
}
}
return result;
}
export function updateTextField(doc: BrowserDocumentApi, tag: string, value: string) {
return updateControls(doc, tag, 'text', (control) =>
Promise.resolve(doc.contentControls.text.setValue({ target: control.target, value })),
);
}
export function updateCheckboxField(doc: BrowserDocumentApi, tag: string, checked: boolean) {
return updateControls(doc, tag, 'checkbox', (control) =>
Promise.resolve(doc.contentControls.checkbox.setState({ target: control.target, checked })),
);
}
export function didUpdateEveryMatch(result: { failures: readonly string[]; matched: number }) {
return result.matched > 0 && result.failures.length === 0;
}
export function describeUpdate(result: FieldUpdateResult) {
if (result.failures.length > 0 && result.matched === 0) return 'The document could not be updated.';
if (result.matched === 0) return 'No matching controls.';
if (result.failures.length > 0) return `Updated ${result.updated} of ${result.matched} locations.`;
if (result.updated === 0) return `${result.matched} locations already match.`;
return `Updated ${result.updated} ${result.updated === 1 ? 'location' : 'locations'}.`;
}
Use the same lookup and receipt pattern for other supported field types. Check controlType, call the matching typed
operation, and use the returned receipt to decide what the application reports.
The content-controls example contains the complete Vanilla TypeScript workflow. It also focuses repeated occurrences, exports the DOCX, and verifies the result after reopening it.
Continue with Replace clauses from your application for block-level content. Use custom content-control UI when your application needs a persistent field list or richer navigation.