Content controls

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.

Fill the templateOpening template...
Application form

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.

On this page