Hide Empty Word Regions and Identify Merge Fields with JavaScript in React

Templates usually carry a few lines that only make sense when there is data behind them — a supplementary note, a remark, an optional cc. With no data, the result you want is for that line and the space it takes up to disappear, rather than leaving a blank stretch that pushes the layout apart.

The related question is: when someone hands you a template, how do you find out which fields it holds and how they are grouped into regions? Expanding the field codes one by one to count them is not an option.

Spire.Doc for JavaScript does both — it hides what the merge has emptied and reports the structure — entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage the template document, with 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.


Hide Empty Paragraphs and Regions After the Merge

Prepare the Template

On top of an ordinary contact letter, the template adds three constructs that may come out empty:

To: «Contact Name»
From: Customer Service
Subject: Requirement Confirmation
Fax.: «Fax»          Date: «Date»

«Optional Note»

«GroupStart:OptionalBlock»
«Optional Line 1»
«Optional Line 2»
«GroupEnd:OptionalBlock»

Note: «Optional Remark»

Message:
Dear «Contact Name»,
Thank you for getting back to us. We will contact you within two working days to confirm the exact delivery arrangements.
Sincerely,
Customer Service

The three are written differently, and between them they cover three cases:

  1. «Optional Note» occupies a paragraph of its own — after the merge not a single character is left inside it;
  2. the region OptionalBlock — fenced by the two fields «GroupStart:OptionalBlock» and «GroupEnd:OptionalBlock», with both paragraphs in between holding nothing but a single field. A region marker is a MERGEFIELD field in Word just like any other; only the field name is written as GroupStart:region name / GroupEnd:region name, and without quotation marks;
  3. Note: «Optional Remark» — the field and the label share one paragraph, so the line still holds the label Note: after the merge.

The Code

function App() {
  const HideEmptyRegions = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }
    // Load the sample template into the virtual file system (VFS)
    let inputFileName = "HideEmptyRegions.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a document and load the template
    let doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Provide values for three fields only; the optional fields in the
    // template are left empty
    let fieldNames = ["Contact Name", "Fax", "Date"];
    let fieldValues = ["John Smith", "+1 (69) 123456", docModule.DateTime.get_Now().Date.ToString()];

    // Hide the paragraphs and the region that come out empty
    doc.MailMerge.HideEmptyParagraphs = true;
    doc.MailMerge.HideEmptyGroup = true;

    // Execute the mail merge
    doc.MailMerge.Execute(fieldNames, fieldValues);

    // Define the output file name
    const outputFileName = "HideEmptyRegions-result.docx";

    // Save the document
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose()

    // Read the saved file and convert it to a Blob
    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>Merge data and hide empty regions</h1>
      <button onClick={HideEmptyRegions}>Generate</button>
    </div>
  );
}
export default App;

Everything hangs on the two lines before Execute:

document.MailMerge.HideEmptyParagraphs = true;
document.MailMerge.HideEmptyGroup = true;

Both are boolean properties on MailMerge, both default to false, and both have to be turned on explicitly. Note that they must be set before Execute — the hiding happens during the merge, so changing them afterwards makes no difference.

fieldNames lists only three "main" fields; Optional Note, Optional Line 1/2 and Optional Remark are given no values at all, and therefore all come out empty after the merge.

The Result

The output with both switches off (the control) set against the output with both on, compared paragraph by paragraph:

What the template says Both switches off Both switches on
«Optional Note» (a paragraph of its own) kept as a blank line the whole paragraph disappears
the region OptionalBlock (marker + two field paragraphs + closing marker) kept as 4 blank lines all four paragraphs disappear
Note: «Optional Remark» Note: Note: (kept)

With the switches on, only these paragraphs are left in the body:

To: John Smith
From: Customer Service
Subject: Requirement Confirmation
Fax.: +1 (69) 123456          Date: 10/12/2026 12:00:00 AM
Note: 
Message:
Dear John Smith,
Thank you for getting back to us. We will contact you within two working days to confirm the exact delivery arrangements.
Sincerely,
Customer Service

Two rules worth remembering:

  • The test is whether the paragraph still holds text after the merge, not whether the field received a value. In the third construct Optional Remark is just as empty, but because the same paragraph still holds the label Note:, the line is kept — only the field's position is left blank. To keep the label itself conditionally, the label has to be turned into a field value of its own.
  • HideEmptyParagraphs is the property that actually deletes. Setting HideEmptyGroup to true on its own while leaving HideEmptyParagraphs = false produces output identical to leaving both switches off — the region still leaves its four blank lines behind. Only with both turned on does the region vanish together with the paragraphs inside it. So if all you want is for the blank lines to go away and you are not using regions, turning on HideEmptyParagraphs is enough.

Word output after the paragraphs and the region that merged to nothing have been hidden

Word output after the paragraphs and the region that merged to nothing have been hidden


Read the Region Names and the Merge Field Name List

The Approach

MailMerge has three methods for reading, each at a different granularity:

Method What it returns
GetMergeGroupNames() every region name in the document
GetMergeFieldNames({ groupName: "…" }) the merge field names inside the named region
GetMergeFieldNames() every merge field name in the document

The second method takes the region name inside an object, as a named argument. That is how the WebAssembly build handles an overloaded method — methods sharing a name are dispatched by argument count and argument name, so the call has to be written as { groupName: "Products" } rather than by passing a plain string.

Prepare the Template

The template defines two regions, each holding several fields:

«GroupStart:Products»
Name: «Contact Name»
Number: «Number»
Type: «Type»
Price: «Price»
Date: «Date»
Vendor: «Vendor»
«GroupEnd:Products»

«GroupStart:TeamRanking»
Name: «Name»
Score: «Score»
Ranking: «Ranking»
Award: «Award»
«GroupEnd:TeamRanking»

Every field gets a paragraph of its own, and the region markers are paragraphs of their own too.

The Code

This sample reads the structure of a document and produces text, so the result goes into a TXT file:

function App() {
  const IdentifyMergeFieldName = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }
    // Load the sample template into the virtual file system (VFS)
    let inputFileName = "IdentifyMergeFieldNames.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // Create a document and load the template
    let doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // Read all region names
    let GroupNames = doc.MailMerge.GetMergeGroupNames();

    // Read the merge field names inside one region
    let MergeFieldNamesWithinRegion = doc.MailMerge.GetMergeFieldNames({ groupName: "Products" });

    // Read every merge field name in the document
    let MergeFieldNames = doc.MailMerge.GetMergeFieldNames();

    // Concatenate the results into text
    let content = [];
    content.push("----------------Group Names-----------------------------------------\n");
    for (let i = 0; i < GroupNames.length; i++) {
      content.push(GroupNames[i] + "\n");
    }
    content.push("----------------Merge field names within a specific group-----------\n");
    for (let j = 0; j < MergeFieldNamesWithinRegion.length; j++) {
      content.push(MergeFieldNamesWithinRegion[j] + "\n");
    }
    content.push("----------------All of the merge field names------------------------\n");
    for (let k = 0; k < MergeFieldNames.length; k++) {
      content.push(MergeFieldNames[k] + "\n");
    }

    // Write the TXT file
    const outputFileName = "IdentifyMergeFieldName-result.txt";
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, content.join("\n"));

    // Close the document
    doc.Close();

    doc.Dispose()

    // Read the saved file and convert it to a Blob
    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], {type: "text/plain"});
    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>Read the region and merge field names from a template</h1>
      <button onClick={IdentifyMergeFieldName}>Generate</button>
    </div>
  );
}
export default App;

Two things differ from the earlier samples:

  • There is no call to Execute. This only reads the template's structure without merging any data, so the three lists can be read straight after loading.
  • The output goes through window.dotnetRuntime.Module.FS.writeFile(fileName, text), writing the string directly into the virtual file system before a text/plain Blob triggers the download. The sample finishes with document.Close(), because it never changes the document's content.

All three methods return a collection (GroupNames.length, indexed access), walked in the same way as Sections and Paragraphs.

The Result

The output file is laid out like this (the ---…--- lines are separators):

----------------Group Names-----------------------------------------

Products

TeamRanking

----------------Merge field names within a specific group-----------

Contact Name

Number

Type

Price

Date

Vendor

----------------All of the merge field names------------------------

Products

Contact Name

Number

Type

Price

Date

Vendor

Products

TeamRanking

Name

Score

Ranking

Award

TeamRanking

The three lists come out as:

Region names (2)
  Products, TeamRanking

Field names inside the Products region (6)
  Contact Name, Number, Type, Price, Date, Vendor

All field names (14 entries)
  Products, Contact Name, Number, Type, Price, Date, Vendor, Products,
  TeamRanking, Name, Score, Ranking, Award, TeamRanking

Two points are worth noting:

  • GetMergeFieldNames() with no argument counts the region markers themselves as field names. GroupStart:Products is a MERGEFIELD field in the document, so every region name turns up twice — once for the opening marker and once for the closing one. Among the 14 entries above, Products and TeamRanking each appear twice. To get the real field names, either read region by region with the groupName version, or filter the region names out of the full list.
  • The groupName version returns only the real field names inside that region — none of the six results for Products contains a GroupStart: or GroupEnd: marker.

Why is there an extra blank line between the entries? While the string is being assembled, content.push(name + "\n") already puts a newline at the end of every entry, and content.join("\n") adds another one between the elements, so two newlines end up in a row. For a compact list, drop the "\n" from the push call:

content.push(GroupNames[i]);

Word output after the region and the merge field names have been read from the template

Word output after the region and the merge field names have been read from the template


FAQ

The hiding switches are on and a blank line is still there

Cause: most likely that line still holds other text. The test for hiding is whether the paragraph is empty after the merge — as long as a single character is left in it (a fixed label such as Note:, for example), the whole paragraph is kept.

Solution: Check that the line you want hidden holds nothing but the field. If the label really does have to disappear together with the field, turn the label into a field value and pass it in as well:

let fieldNames = ["Contact Name", "Remark Label", "Remark"];
let fieldValues = ["John Smith", "Note: ", ""];

Setting HideEmptyGroup = true on its own has no effect

Cause: HideEmptyParagraphs is what actually deletes paragraphs. A region disappears as a whole because the paragraphs inside it are deleted one by one; the markers themselves trigger no extra deletion.

Solution: Turn both properties on. That is exactly why the sample writes both lines:

document.MailMerge.HideEmptyParagraphs = true;
document.MailMerge.HideEmptyGroup = true;

Should the hiding switches be set before or after Execute

Before Execute. The hiding of empty paragraphs and regions happens during the merge, so changing the properties once it has finished has no effect on the result that has already been produced.

GetMergeFieldNames errors out or returns nothing when a string is passed

Cause: It is an overloaded method. The WebAssembly build dispatches by argument name, so the region name has to be passed in an object with a key.

Solution: Write it as an object:

let fields = document.MailMerge.GetMergeFieldNames({ groupName: "Products" });

The region name must match the name after GroupStart: in the template exactly, case included.

Region names turn up in the result of GetMergeFieldNames()

Cause: Both the opening and the closing marker of a region are merge fields themselves, so they are counted into the full list and every region name appears twice.

Solution: To filter them out, collect the result of GetMergeGroupNames() into a set and drop those names:

let groups = document.MailMerge.GetMergeGroupNames();
let groupSet = {};
for (let i = 0; i < groups.length; i++) groupSet[groups[i]] = true;

let names = document.MailMerge.GetMergeFieldNames();
let realFields = [];
for (let i = 0; i < names.length; i++) {
  if (!groupSet[names[i]]) realFields.push(names[i]);
}

If you only need the fields of one particular region, the groupName version is less trouble — its result never contains markers.

The document defines regions but GetMergeGroupNames() returns 0

Cause: A region marker has to be written as MERGEFIELD GroupStart:region name, with the field name left unquoted. Written as MERGEFIELD "GroupStart:Products" (with quotes) it is treated as an ordinary merge field name and does not form a region.

Solution: Check the field codes of the two markers in the template:

{ MERGEFIELD  GroupStart:Products  \* MERGEFORMAT }
{ MERGEFIELD  GroupEnd:Products  \* MERGEFORMAT }

Also, GroupStart and GroupEnd have to come as a pair, with the region name written out in full and in matching case; a marker without its counterpart is not recognised as a region.

The TXT output opens as a single line in some editors

Cause: This sample writes plain text, and the line separator matters if the file is later saved as .txt and opened in Windows Notepad; the text Spire writes uses \n.

Solution: Open it in an editor such as VS Code or Notepad++, or replace "\n" with "\r\n" while the string is being assembled.


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.