Replace Placeholders in Word Documents with HTML or Paragraphs from Another Document Using JavaScript in React

Replacing placeholders in documents with HTML content or paragraphs from another document is a highly practical need in document automation — for example, inserting rich HTML content authored in a WYSIWYG editor (such as TinyMCE or Quill) into placeholder positions in a Word template, or extracting specific paragraphs from a standard clause library and replacing corresponding placeholders in a contract template. Spire.Doc for JavaScript handles such replacement operations entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts and document files — no backend server required.

This article covers two core features:

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


Replace Placeholder with HTML

Replacing a placeholder with HTML involves three stages: first, load the font files, the HTML file, and the target Word document into the WASM virtual file system via FetchFileToVFS; then, create a temporary Section, render the HTML string into document objects using AppendHTML, collect them into a replacement list, find all [#placeholder] occurrences with FindAllString, sort the matched positions, and insert the replacement content one by one via ChildObjects.Insert while removing the original text; finally, remove the temporary Section, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.

import React, { useState } from 'react';

function App() {
  // Define the placeholder replacement logic
  function ReplacedWithHTML(location, replacement) {
    let textRange = location.Text;
    let index = location.Index;
    let paragraph = location.Owner;
    let sectionBody = paragraph.OwnerTextBody;
    let paragraphIndex = sectionBody.ChildObjects.IndexOf(paragraph);

    let replacementIndex = -1;
    if (index === 0) {
      paragraph.ChildObjects.RemoveAt(0);
      replacementIndex = sectionBody.ChildObjects.IndexOf(paragraph);
    } else if (index === paragraph.ChildObjects.Count - 1) {
      paragraph.ChildObjects.RemoveAt(index);
      replacementIndex = paragraphIndex + 1;
    } else {
      let paragraph1 = paragraph.Clone();
      while (paragraph.ChildObjects.Count > index) {
        paragraph.ChildObjects.RemoveAt(index);
      }
      let i = 0;
      let count = index + 1;
      while (i < count) {
        paragraph1.ChildObjects.RemoveAt(0);
        i += 1;
      }
      sectionBody.ChildObjects.Insert(paragraphIndex + 1, paragraph1);
      replacementIndex = paragraphIndex + 1;
    }

    for (let i = 0; i <= replacement.length - 1; i++) {
      sectionBody.ChildObjects.Insert(replacementIndex + i, replacement[i].Clone());
    }
  }

  function TextRangeLocation(TextRange) {
    this.Text = TextRange;
    this.Owner = this.Text.OwnerParagraph;
    this.Index = this.Owner.ChildObjects.IndexOf(this.Text);
    this.CompareTo = function (other) {
      return -(this.Index - other.Index);
    };
  }

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

    // Check if the module is ready
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the font file into the virtual file system (VFS)
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // Load the HTML file and Word document into VFS
    let HTMLName = 'InputHtml.txt';
    await window.spire.FetchFileToVFS(HTMLName, '', `${process.env.PUBLIC_URL}/data/`);
    const HTML = window.dotnetRuntime.Module.FS.readFile(HTMLName);

    let inputFileName = 'ReplaceWithHtml.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // Load the document
    let doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Create a temporary Section and render the HTML
    let replacement = [];
    let tempSection = doc.AddSection();
    let par = tempSection.AddParagraph();
    const decoder = new TextDecoder('utf-8');
    const HTMLString = decoder.decode(HTML);
    par.AppendHTML(HTMLString);

    // Collect the rendered document objects
    for (let i = 0; i < tempSection.Body.ChildObjects.Count; i++) {
      let docObj = tempSection.Body.ChildObjects.get_Item(i);
      replacement.push(docObj);
    }

    // Find all placeholders and sort
    let selections = doc.FindAllString('[#placeholder]', false, true);
    let locations = [];
    for (let selection of selections) {
      locations.push(new TextRangeLocation(selection.GetAsOneRange()));
    }
    locations.sort();

    // Replace one by one
    for (let location of locations) {
      ReplacedWithHTML(location, replacement);
    }

    // Remove the temporary Section
    doc.Sections.Remove(tempSection);

    // Define the output file name and save
    const outputFileName = 'ReplaceWithHtml_output.docx';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    // Read the generated file from VFS and trigger download
    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { 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>Replace Placeholder with HTML in a Word Document</h1>
      <button onClick={ReplaceWithHTML}>
        Generate
      </button>
    </div>
  );
}

export default App;

The [#placeholder] placeholders in the document are replaced with rich text content rendered from HTML

The [#placeholder] placeholders in the document are replaced with rich text content rendered from HTML


Replace Placeholder with Paragraphs from Another Document

Replacing a placeholder with paragraphs from another document involves three stages: first, load the font files and two Word documents into the WASM virtual file system via FetchFileToVFS; then, load the main document and the source document separately, use FindAllPattern with a regular expression to find placeholders (such as [MY_DOCUMENT]), iterate through all Sections and Paragraphs of the source document, insert each paragraph into the corresponding position in the main document using ChildObjects.Insert, and finally remove the original placeholder text; lastly, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.

import React, { useState } from 'react';

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

    // Check if the module is ready
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the two Word documents into VFS
    let inputFileName1 = 'ReplaceContentWithDoc.docx';
    await window.spire.FetchFileToVFS(inputFileName1, '', `${process.env.PUBLIC_URL}/data/`);

    let inputFileName2 = 'Insert.docx';
    await window.spire.FetchFileToVFS(inputFileName2, '', `${process.env.PUBLIC_URL}/data/`);

    // Load the main document
    let document1 = new docModule.Document();
    document1.LoadFromFile(inputFileName1);

    // Load the source document (contains paragraphs to insert)
    let document2 = new docModule.Document();
    document2.LoadFromFile(inputFileName2);

    // Get the first Section of the main document
    let section1 = document1.Sections.get_Item(0);

    // Create a regex to find the placeholder
    let regex = new docModule.Regex('\\[MY_DOCUMENT\\]', docModule.RegexOptions.None);

    // Find all matching placeholders
    let textSections = document1.FindAllPattern({ pattern: regex });

    // Iterate through each match
    for (let i = 0; i < textSections.length; i++) {
      let selection = textSections[i];
      let para = selection.GetAsOneRange().OwnerParagraph;
      let textRange = selection.GetAsOneRange();
      let index = section1.Body.ChildObjects.IndexOf(para);

      // Insert all paragraphs from the source document at the placeholder position
      for (let i = 0; i < document2.Sections.Count; i++) {
        let section2 = document2.Sections.get_Item(i);
        for (let j = 0; j < section2.Paragraphs.Count; j++) {
          let paragraph = section2.Paragraphs.get_Item(j);
          section1.Body.ChildObjects.Insert(index++, paragraph.Clone());
        }
      }

      // Remove the original placeholder text
      para.ChildObjects.Remove(textRange);
    }

    // Define the output file name and save
    const outputFileName = 'ReplaceContentWithDoc_output.docx';
    document1.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    document1.Dispose();

    // Read the generated file from VFS and trigger download
    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { 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>Replace Placeholder with Paragraphs from Another Document</h1>
      <button onClick={ReplaceContentWithDoc}>
        Generate
      </button>
    </div>
  );
}

export default App;

The [MY_DOCUMENT] placeholder in the main document is replaced with all paragraphs from the source document

The [MY_DOCUMENT] placeholder in the main document is replaced with all paragraphs from the source document


FAQ

HTML content formatting is not displayed correctly

Cause: The AppendHTML method supports a limited range of HTML tags, only recognizing basic block-level and inline tags (such as <p>, <b>, <i>, <table>, etc.). Complex CSS styles, JavaScript code, or HTML5-specific tags are ignored.

Solution: Ensure the input HTML uses only basic tags and defines formatting through inline styles (such as style="color:red") rather than CSS class names:

<p style="font-size:14pt; color:#2E75B6;">This is blue heading text</p>
<ul><li>Item one</li><li>Item two</li></ul>

Inserted paragraph order does not match expectations

Cause: When replacing multiple placeholders, the matched positions are not sorted before processing. Replacing sequentially from beginning to end causes the indices of subsequent positions to shift, leading to paragraphs being inserted at incorrect locations.

Solution: Sort all matched positions in descending order by index (replacing from the end of the document backward), or track the original index offset for each position:

let locations = [];
for (let selection of selections) {
  locations.push(new TextRangeLocation(selection.GetAsOneRange()));
}
locations.sort(); // Descending order, replace from back to front

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.