Templates and fields

Add fields to a DOCX template

Turn document selections into tagged inline and block-level content controls.

Use this workflow when your DOCX does not yet have fields your application can find. Wrap a client name in an inline field, then create a block field for a confidentiality clause. If your template already has fields, start with Fill a DOCX template.

Add both field shapes

Select the client name and add the inline field. Then place the caret on the empty line below Confidentiality and add the block field.

Add template fieldsOpening document...

Create the fields

Use the complete Vanilla TypeScript example and choose Add fields. It includes the untagged agreement, selection controls, Editor setup, and export button.

Pass the current selection target to create.contentControl(). Here, the inline call wraps a text selection. The block call targets an empty paragraph and replaces it with the supplied HTML content:

import type { BrowserDocumentApi, SelectionTarget } from 'superdoc/ui';

export async function addClientNameField(doc: BrowserDocumentApi, selection: SelectionTarget) {
  return doc.create.contentControl({
    kind: 'inline',
    controlType: 'text',
    tag: 'client.legalName',
    alias: 'Client legal name',
    at: selection,
  });
}

export async function addConfidentialityField(doc: BrowserDocumentApi, caret: SelectionTarget) {
  return doc.create.contentControl({
    kind: 'block',
    controlType: 'richText',
    tag: 'agreement.confidentiality',
    alias: 'Confidentiality clause',
    html: '<p>Each party will protect confidential information with reasonable care.</p>',
    at: caret,
  });
}

kind controls placement. controlType controls field behavior:

PropertyThis exampleMeaning
kindinline, blockWhere the control sits in the document structure
controlTypetext, richTextWhich content-control operations apply
tagclient.legalNameApplication lookup and grouping key
aliasClient legal nameReadable title stored in the DOCX
idAssigned on creationOne control occurrence in the document

The example obtains the selection from the Editor-owned UI controller after readiness. The helper needs a current selection target, not the selected text alone. Check the creation receipt before treating the field as available.

Replace a selection with a formatted locked value

For an inline replacement or insertion, pass content. When inline is omitted, the new text inherits the destination's document run formatting: insertion uses the run formatting at the caret, and replacement uses the first selected character's formatting. Character-style references are retained, so inherited styles continue to apply. Automatic inheritance excludes pending browser caret overrides, such as italic toggled before typing.

const target = (await editor.doc.selection.current()).selectionTarget;
if (!target) throw new Error('Select text or place the caret first.');
const receipt = await editor.doc.create.contentControl({
  kind: 'inline',
  controlType: 'text',
  at: target,
  content: '12,345',
  tag: 'linked-value',
  lockMode: 'contentLocked',
});
if (!receipt.success) throw new Error('The linked value could not be created.');

The creation call commits the inherited or explicit formatting, text, tag, and lock in one undo entry. One Undo restores the original content; Redo restores the complete control. contentLocked prevents content edits and permits removing the wrapper. inline requires explicit plain-text content and kind: 'inline'; wrapping an existing selection without replacement preserves its existing formatting.

To supply formatting explicitly, pass inline with fontFamily, fontSize, bold, italic, underline, or strike, using the same value shapes as format.apply. An explicit payload replaces automatic run-format inheritance; properties omitted from that payload inherit paragraph formatting.

To capture pending caret overrides or reject mixed formatting, read the current browser selection and pass its uniform values through inline:

const target = (await editor.doc.selection.current()).selectionTarget;
if (!target) throw new Error('Select text or place the caret first.');
const keys = ['fontFamily', 'fontSize', 'bold', 'italic', 'underline', 'strike'] as const;
const captured = await editor.doc.format.readSelection({ selection: 'current', keys: [...keys] });
const inline = {};
for (const key of keys) {
  const property = captured.properties[key];
  if (property?.state !== 'uniform') throw new Error(`Cannot resolve ${key}.`);
  Object.assign(inline, { [key]: property.value });
}
const receipt = await editor.doc.create.contentControl({
  kind: 'inline',
  controlType: 'text',
  at: target,
  content: '12,345',
  inline,
  tag: 'linked-value',
  lockMode: 'contentLocked',
});
if (!receipt.success) throw new Error('The linked value could not be created.');

Keep the formatting read and selection target at the same document revision. If automatic inheritance cannot resolve the destination's formatting, creation returns CAPABILITY_UNAVAILABLE without changing the document or history. Pass explicit inline values to bypass automatic inheritance.

Turn a newly typed suggestion into a field

In Suggesting mode, type receiving_party into an ordinary body paragraph. Select the entire token once and wait for the selection to settle before reading superdoc.ui.selection.getSnapshot():

async function convertSelectedPendingToken(superdoc) {
  const selection = superdoc.ui.selection.getSnapshot();
  const at = selection?.selectionTarget;
  if (selection?.status !== 'ready' || selection.quotedText !== 'receiving_party' || !at) {
    return { kind: 'selection-not-ready' };
  }
  try {
    const receipt = await superdoc.activeEditor.doc.create.contentControl({
      kind: 'inline',
      controlType: 'text',
      at,
      alias: 'receiving_party',
      content: 'receiving_party',
      lockMode: 'unlocked',
    });
    if (!receipt.success) return { kind: 'refused', failure: receipt.failure };
    return { kind: 'converted', receipt };
  } catch (error) {
    return { kind: 'fault', error };
  }
}

Omitting the second argument inherits the editor's Suggesting mode. You can also pass { changeMode: 'tracked' } as the second argument. superdoc.ui.selection.current() synchronously returns the best-known cached selection data; awaiting it does not wait for worker selection reads. Use the settled getSnapshot() flow above and its status === 'ready' check before using the selection target.

This conversion supports one complete, simple insertion created by the current author in the current session, with a unique revision identity. Supply identical plain-text content and omit inline: conversion preserves the original formatted runs, including existing bold and italic within the token. Pending tracked formatting changes, such as bold applied to the token while in Suggesting mode, are refused.

Conversion preserves the original Word revision IDs and source insertion ID. The public canonical review ID changes to identify a structural content-control insertion; use the returned reference after conversion.

The field and token share one review decision. Accept keeps both; Reject removes both. The conversion receipt's trackedChangeRefs identifies the current review reference. Use that reference or read the current review list before deciding. One Undo restores the original text suggestion, and one Redo restores the field.

Partial selections, selections spanning multiple revisions, foreign or imported insertions, ambiguous or split revisions, pending tracked formatting changes, and complex content such as nested controls or fields are refused. Linked revisions, including a pending paragraph decision or the inserted half of a paired replacement, are also refused. Changed content, rich-text payloads and explicit inline formatting overrides are unsupported for this conversion. A refusal returns success: false with a CAPABILITY_UNAVAILABLE failure receipt and leaves the document, history and review state unchanged. Check receipt.failure to show the reason. You can select a supported complete insertion or create a separate field at a collapsed selection outside pending revisions. Previews with dryRun: true are also atomic and leave the document, history and review state unchanged.

Tracked range creation over accepted text is also refused when that paragraph contains another pending text revision or paragraph decision, even when the selection does not overlap it. Accepted-token replacement remains supported in paragraphs without pending revisions; separate-field insertion outside revision wrappers uses a collapsed selection.

Tracked range replacement with different content is refused atomically until the field and replaced text can remain one decision through export/reopen. Use identical plain-text content for token conversion or accepted-token replacement, or a collapsed selection for a separate field.

Handle exceptions separately from refusal receipts: malformed input can throw a validation error, and transport or runtime faults can reject the call. The example returns those exceptions as kind: 'fault'. The private offsetSpace option is not consumed by this operation and cannot enable an unsupported conversion.

Keep the fields in your template

Export the DOCX from the standalone example and reopen it. Confirm that the client name is an inline text control and the confidentiality clause is a block rich-text control, with the tags and titles shown above. Keep this file as the template you fill later; reopening the original draft will not include the fields you just added.

Continue with Fill a DOCX template to update fields from application data. Use the create.contentControl() reference for every input shape.

On this page