Read and Modify Content Controls in Word Documents with JavaScript in React

A document generated from a template and filled in by several people often contains dozens of content controls. Checking which fields have been changed and which controls still carry the template tags one by one through the properties panel is inefficient. If the alias, ID, tag and type of every control can be exported into a single table, the review work becomes much lighter. The same traversal can also be used in reverse: batch-modify the color and appearance of controls by type, so that fields with different purposes can be told apart at a glance in the document.

This article uses Spire.Doc for JavaScript to accomplish three tasks: read control properties and export a list, modify control color by type, and set display appearance by type. The document is loaded through a virtual file system in the browser, and the results are downloaded directly without going through a server.

This article covers three features:

Reading and modification rely on the same traversal logic, so the object types of the two kinds of controls should be clarified first:

Object type DocumentObjectType value Location
Inline content control StructureDocumentTagInline In the ChildObjects of a paragraph
Block-level content control StructureDocumentTag In the ChildObjects of a section body or a table cell

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume that Spire.Doc is installed and the WebAssembly module is initialized.


Read Content Control Properties

The goal of reading is to summarize the key properties of every content control in the document into a text file. The example reads ContentControl.docx from public/data; place that file in the project's public/data directory before running it. Three storage locations need to be handled: inline controls in body paragraphs, inline controls in table cells, and block-level controls in the body. Only by traversing all three will the list be complete.

The properties that are read include:

Property Meaning
SDTProperties.Alias The control alias, the name displayed in Word
SDTProperties.Id The control ID, unique within the document
SDTProperties.Tag The control tag, usually written in advance by the template
SDTProperties.SDTType The control type, such as Text or DropDownList

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font, read ContentControl.docx into the VFS and load it as a Document.
  3. Traverse body paragraphs and table cells to collect inline controls, then collect block-level controls in the body.
  4. Read the alias, ID, tag and type of each control in turn and join them into tab-separated text.
  5. Write the text to the VFS and trigger a download to get a list file that can be imported into spreadsheet software.

The code for reading content control properties is as follows:

function App() {
  const readContentControlProperties = async () => {
    // Get the Spire.Doc WASM module
    const wasmModule = window.wasmModule?.spiredoc;

    if (!wasmModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font and the document to be read
    await window.spire.FetchFileToVFS('ContentControl.docx', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new wasmModule.Document();
    doc.LoadFromFile('ContentControl.docx');

    // Collect inline controls and block-level controls
    const found = [];
    const blocks = [];

    const scanParagraph = (paragraph) => {
      for (let j = 0; j < paragraph.ChildObjects.Count; j++) {
        const child = paragraph.ChildObjects.get_Item(j);
        if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
          found.push(child);
        }
      }
    };

    const scanTable = (table) => {
      for (let r = 0; r < table.Rows.Count; r++) {
        const row = table.Rows.get_Item(r);
        for (let c = 0; c < row.Cells.Count; c++) {
          const cell = row.Cells.get_Item(c);
          for (let k = 0; k < cell.ChildObjects.Count; k++) {
            const child = cell.ChildObjects.get_Item(k);
            if (child.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) scanParagraph(child);
            if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) found.push(child);
          }
        }
      }
    };

    for (let s = 0; s < doc.Sections.Count; s++) {
      const section = doc.Sections.get_Item(s);
      for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
        const obj = section.Body.ChildObjects.get_Item(i);
        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) scanParagraph(obj);
        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Table) scanTable(obj);
        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) blocks.push(obj);
      }
    }

    // Build the property list
    let report = 'Alias\tID\tTag\tType\r\n';
    for (const sdt of [...found, ...blocks]) {
      const properties = sdt.SDTProperties;
      const type = String(properties.SDTType).replace('SdtType.', '');
      report += `${properties.Alias}\t${properties.Id}\t${properties.Tag}\t${type}\r\n`;
    }

    // Write to the VFS and download the text file
    window.dotnetRuntime.Module.FS.writeFile('GetContentControlProperty.txt', report);
    doc.Dispose();

    const fileArray = window.dotnetRuntime.Module.FS.readFile('GetContentControlProperty.txt');
    const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'GetContentControlProperty.txt';
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Read Content Control Properties</h1>
      <button onClick={readContentControlProperties}>Export Property List</button>
    </div>
  );
}

export default App;

The body and table of the sample document are shown below:

Employee onboarding information form to be read

The exported list is as follows:

Alias ID Tag Type
Name 2001 employee-name Text
Department 2002 department DropDownList
Hire Date 2003 hire-date DatePicker
Laptop 2004 laptop CheckBox
External Monitor 2005 monitor CheckBox
Onboarding Notice 2006 onboarding-notice RichText

How to Cover Tables and Block-Level Controls During Traversal

Paragraphs directly in the body cover only some of the controls. Controls in a table require entering Table.Rows and Row.Cells before scanning the ChildObjects of the cell; block-level controls appear directly in Body.ChildObjects, and their content is not inside a paragraph. The example handles the three cases separately and merges the results for output.

During traversal, check DocumentObjectType before accessing properties. Reading SDTProperties on an ordinary paragraph returns an empty value or raises an exception; filtering by type first avoids such problems.


Modify Content Control Color by Type

When a document contains text, rich text, date and other controls at the same time, telling purposes apart by color is more intuitive than reading aliases one by one. The example reads ModifySTDColor.docx from public/data; place that file in the project's public/data directory before running it. For example, mark all text controls green and all rich text controls orange. The color is written to SDTProperties.Color and assigned according to the control type.

Modifying the color involves the following members:

Member Purpose
SDTProperties.SDTType Determines the control type and therefore which color to assign
SDTProperties.Color The control color, constructed with Color.FromArgb()
Color.FromArgb(a, r, g, b) Creates a color from ARGB components

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font, read ModifySTDColor.docx into the VFS and load it as a Document.
  3. Traverse paragraphs and block-level controls in the body and take out each content control object.
  4. Branch on SDTType: assign green to text controls and orange to rich text controls.
  5. Save the result file with SaveToFile(), read it from the VFS and trigger a browser download.

The code for modifying content control color by type is as follows:

function App() {
  const modifySDTColor = async () => {
    // Get the Spire.Doc WASM module
    const wasmModule = window.wasmModule?.spiredoc;

    if (!wasmModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font and the document to be processed
    await window.spire.FetchFileToVFS('ModifySTDColor.docx', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new wasmModule.Document();
    doc.LoadFromFile('ModifySTDColor.docx');

    // Iterate over the sections and modify control colors by type
    for (let s = 0; s < doc.Sections.Count; s++) {
      const section = doc.Sections.get_Item(s);
      for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
        const obj = section.Body.ChildObjects.get_Item(i);

        const applyColor = (sdt) => {
          switch (sdt.SDTProperties.SDTType) {
            case wasmModule.SdtType.RichText:
              sdt.SDTProperties.Color = wasmModule.Color.FromArgb(255, 237, 125, 49);
              break;
            case wasmModule.SdtType.Text:
              sdt.SDTProperties.Color = wasmModule.Color.FromArgb(255, 0, 128, 0);
              break;
          }
        };

        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) {
          applyColor(obj);
        }

        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) {
          for (let j = 0; j < obj.ChildObjects.Count; j++) {
            const child = obj.ChildObjects.get_Item(j);
            if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
              applyColor(child);
            }
          }
        }
      }
    }

    // Save the result file
    const outputFileName = 'ModifySDTColor_out.docx';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
    doc.Dispose();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Modify Content Control Color by Type</h1>
      <button onClick={modifySDTColor}>Generate Document</button>
    </div>
  );
}

export default App;

After the color is modified, the document looks like this: the tags of text controls are green and the tags of rich text controls are orange:

Document after modifying content control color by type

Control Color and Text Color Are Not the Same Thing

SDTProperties.Color sets the color of the control tag, not the color of the text inside the control. Text color belongs to TextRange.CharacterFormat.TextColor and has to be set for each text fragment. The two handle different parts: the former identifies the control itself, and the latter controls body typography.

The control color is visible only when Appearance is Tags. If the document uses the default appearance, the color will not show even after it is assigned; set SdtAppearance.Tags as well, or use the border appearance described later.


Set Content Control Appearance by Type

Appearance determines how content controls are presented in the Word editing view, and the example also reads ContentControl.docx from public/data. During the filling stage, field boundaries should be clear, so text, drop-down list and date controls can be set to the tag appearance; during the review stage, the layout should be clean, so check boxes can be set to the hidden appearance, keeping only the box glyphs. Setting them by type lets the same document serve different purposes.

Appearance is controlled by SDTProperties.Appearance; the common values are as follows:

Value Display effect Suitable scenario
SdtAppearance.Tags Shows alias tags on both sides of the content Fill-in fields whose names need to be explicit
SdtAppearance.BoundingBox Draws a border around the control range Block-level content, a whole paragraph of explanation
SdtAppearance.Hidden Hides the control frame Controls that already have a visual mark, such as check boxes

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font, read ContentControl.docx into the VFS and load it as a Document.
  3. Traverse paragraphs and block-level controls in the body and take out each content control object.
  4. Branch on SDTType to set Appearance: tags for text, drop-down list and date, border for rich text, hidden for check boxes.
  5. Save the result file with SaveToFile(), read it from the VFS and trigger a browser download.

The code for setting content control appearance by type is as follows:

function App() {
  const setContentControlAppearance = async () => {
    // Get the Spire.Doc WASM module
    const wasmModule = window.wasmModule?.spiredoc;

    if (!wasmModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the document to be processed
    await window.spire.FetchFileToVFS('ContentControl.docx', '', `${process.env.PUBLIC_URL}/data/`);

    const doc = new wasmModule.Document();
    doc.LoadFromFile('ContentControl.docx');

    // Iterate over the sections and set the display appearance by type
    for (let s = 0; s < doc.Sections.Count; s++) {
      const section = doc.Sections.get_Item(s);
      for (let i = 0; i < section.Body.ChildObjects.Count; i++) {
        const obj = section.Body.ChildObjects.get_Item(i);

        const applyAppearance = (sdt) => {
          switch (sdt.SDTProperties.SDTType) {
            case wasmModule.SdtType.Text:
            case wasmModule.SdtType.DropDownList:
            case wasmModule.SdtType.DatePicker:
              sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.Tags;
              break;
            case wasmModule.SdtType.RichText:
              sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.BoundingBox;
              break;
            case wasmModule.SdtType.CheckBox:
              sdt.SDTProperties.Appearance = wasmModule.SdtAppearance.Hidden;
              break;
          }
        };

        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTag) {
          applyAppearance(obj);
        }

        if (obj.DocumentObjectType === wasmModule.DocumentObjectType.Paragraph) {
          for (let j = 0; j < obj.ChildObjects.Count; j++) {
            const child = obj.ChildObjects.get_Item(j);
            if (child.DocumentObjectType === wasmModule.DocumentObjectType.StructureDocumentTagInline) {
              applyAppearance(child);
            }
          }
        }
      }
    }

    // Save the result file
    const outputFileName = 'SetContentControlAppearance_out.docx';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
    doc.Dispose();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Set Content Control Appearance by Type</h1>
      <button onClick={setContentControlAppearance}>Generate Document</button>
    </div>
  );
}

export default App;

The document after setting the appearance is shown below:

Document after setting content control appearance by type

Appearance Only Affects the Editing View

Appearance is a visual hint for editors and does not change the control type, content or lock state. After the appearance is set to Hidden, the control no longer shows a frame in Word, but it can still be selected and filled in; neither printing nor PDF export leaves a tag or border behind. When the filling behavior needs to be restricted, use lock settings instead of relying on appearance.


FAQ

Why is there no visible change after setting the control color

SDTProperties.Color applies to the control tag and is displayed only when Appearance is Tags. If the document still uses the default appearance, the color will not appear on the page. Set Appearance to SdtAppearance.Tags and save the document again, and the tag will carry the configured color.

How to tell whether a control is inline or block-level

Read DocumentObjectType: StructureDocumentTagInline is an inline control located inside a paragraph, while StructureDocumentTag is a block-level control located directly in the section body or a table cell. Both expose their properties through SDTProperties, but they have different traversal entry points, and a block-level control does not go through a paragraph.

Why are the content controls in a table not read

Table content is not in the ChildObjects of body paragraphs; you have to enter Table.Rows, then access Row.Cells, and finally scan Cell.ChildObjects. The scanTable helper in the example handles this layer specifically. If only body paragraphs are traversed, the fields in the table will be missed.

Why does a newly created control read back with a different type

The type is determined by the content of the saved document. When creating a control, set SDTType explicitly, then write the content and save; if the type is omitted or left to the default, some controls are recognized as RichText when read back. When reading an existing document, SDTType reflects the type actually stored in the document, which can be used to verify that the template was generated as expected.


Get a Free License

Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.