Mail Merge Word Documents and Format Values by Culture with JavaScript in React
Whenever the same content has to go out to different recipients, Word's mail merge is the least painful way to do it: the parts that vary (the recipient, the fax number, the date) stay in the template as merge fields, the rest of the layout is fixed, and one set of data is filled in at run time. Doing this in the browser is worth the trouble because the template stays on the server, the data never has to be handed to Office, and the user gets the filled-in document with a single click.
Spire.Doc for JavaScript parses merge fields as a class of document element of their own, and the merge entry point is MailMerge.Execute(fieldNameArray, fieldValueArray): the two arrays correspond by index, and a field that exists in the template but is not supplied in the arrays is merged as empty.
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.
Execute a Mail Merge
Prepare the Template
The template is an ordinary customer contact letter, with the four places that change with the data turned into merge fields:
To: «Contact Name»
From: Customer Service
Subject: Requirement Confirmation
Fax.: «Fax» Date: «Date»
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 «Contact Name», «Fax» and «Date» in the template are how a merge field's placeholder is written in Word (press Ctrl + F9 at the field position, then type MERGEFIELD <name>). Note that Contact Name contains a space, so Word writes it in quoted form in the field code, MERGEFIELD "Contact Name"; when the name is passed to Execute in the code below the quotes are dropped, and only the name itself, "Contact Name", is written.
Code
function App() {
const MailMerge = 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 sample template into the virtual file system (VFS)
let inputFileName = "MailMerge.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document and load the template
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Field names and field values correspond by index
let fieldNames = ["Contact Name", "Fax", "Date"];
let fieldValues = ["John Smith", "+1 (69) 123456", docModule.DateTime.get_Now().Date.ToString()];
// Execute the mail merge
doc.MailMerge.Execute(fieldNames, fieldValues);
// Define the output file name
const outputFileName = "MailMerge-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 into a Word document</h1>
<button onClick={MailMerge}>Generate</button>
</div>
);
}
export default App;
Only one call here is the merge logic itself:
document.MailMerge.Execute(fieldNames, fieldValues);
DateTime.get_Now().Date.ToString() takes the current date and returns a string, which can be passed straight in as a field value — the run of blank space between the Fax value and the Date: label afterwards is decided by the template, and the merge does not touch it.
Result
All four fields are replaced and the rest of the template is kept exactly as it was:
To: John Smith
From: Customer Service
Subject: Requirement Confirmation
Fax.: +1 (69) 123456 Date: 10/10/2026 12:00:00 AM
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
Contact Name appears twice and both occurrences take the same value — fields with the same name share one value, so it does not have to be listed twice in the arrays.
Word output after the contact letter has been merged with the data

Format Values by Culture
The output format of that Date.ToString() call is decided by the culture settings of the runtime environment. The same code gives 10/10/2026 12:00:00 AM in an English environment and 2026/10/10 上午12:00:00 on a Chinese Windows machine. If the recipient is in Germany, the correct spelling is 10.10.2026, 10:50:45 — day first, month in the middle, separated by dots.
The most direct way to control that format is to format the value before it ever reaches Execute: the merge itself only puts a string of text where the field is, and how the date is laid out is a matter for the data preparation stage.
Code
This shares the same MailMerge.docx template as the previous example and only swaps the date for a string formatted for the de-DE culture:
function App() {
const ChangeLocale = 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 sample template into the virtual file system (VFS)
let inputFileName = "MailMerge.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document and load the template
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Prepare the date string in the German format (day.month.year)
const now = new Date();
const datestr = new Intl.DateTimeFormat("de-DE",
{
year: 'numeric', month: '2-digit', day: '2-digit',
hour: "2-digit", minute: '2-digit', second: "2-digit"
}).format(now);
// Field names and field values correspond by index
let fieldNames = ["Contact Name", "Fax", "Date"];
let fieldValues = ["John Smith", "+1 (69) 123456", datestr];
// Execute the mail merge
doc.MailMerge.Execute(fieldNames, fieldValues);
// Define the output file name
const outputFileName = "ChangeLocale-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 a de-DE formatted date into a Word document</h1>
<button onClick={ChangeLocale}>Generate</button>
</div>
);
}
export default App;
The template is exactly the same and the merge call is exactly the same; the only thing that changes is where the date string in fieldValues comes from:
const datestr = new Intl.DateTimeFormat("de-DE", { /* … */ }).format(now);
Intl.DateTimeFormat is the browser's built-in formatting tool: the first argument is the locale code, the second the format options. Besides de-DE, the usual ones include en-US (10/10/2026, 10:50:45 AM), fr-FR, ja-JP and so on — switching the locale code switches the whole date convention.
Result
Same template, same set of data, only the way the Date field is written changes:
| Source of the date field | Output |
|---|---|
DateTime.get_Now().Date.ToString() |
Date: 10/10/2026 12:00:00 AM |
Intl.DateTimeFormat("de-DE", …).format(now) |
Date: 10.10.2026, 10:50:45 |
The merged document:
To: John Smith
From: Customer Service
Subject: Requirement Confirmation
Fax.: +1 (69) 123456 Date: 10.10.2026, 10:50:45
Message:
Dear John Smith,
…
Word output after the date has been formatted for the de-DE culture

FAQ
The merged document still contains the «Contact Name» angle brackets
Cause: A merge field's «name» placeholder is the field's result text (what sits between the MERGEFIELD field code and the end marker). A merge only replaces the field's result area — if the « and » in the template were typed as separate text outside the field, they are not part of that field and are naturally left untouched.
Solution: When editing the template in Word, press Ctrl + F9 at the field position to create the field, fill in the field code, and then press F9 to let Word generate the result text; do not type the angle brackets by hand outside the field. When the template is generated from code, the « and » have to be written between the field's separate and end markers, not as a separate run outside the field.
A field name in the array does not match the template and that position is left unfilled
Execute pairs its two arrays by index, and field names are matched case-sensitively. "Contact Name" and "contact name" are treated as two different fields.
A field that appears in the template but is not listed in fieldNames is merged as empty (its result area becomes an empty string rather than keeping the original placeholder). That is exactly how "leave it blank when there is no data" is implemented, but if the intent is to keep the original text, the field name has to be added to the array.
The field name contains a space — how should the field code in the template be written
In a Word field code, a field name containing a space or any other special character has to be wrapped in double quotes:
{ MERGEFIELD "Contact Name" \* MERGEFORMAT }
In JavaScript, what is passed to Execute is the name itself, without quotes:
let fieldNames = ["Contact Name"];
The date format comes out differently on different machines
DateTime.get_Now().Date.ToString() uses the current culture of the runtime environment, so systems with different regional settings and different browsers produce different date strings. For stable output, do what the second feature in this article does: pin the locale code with Intl.DateTimeFormat and pass the result into Execute. Alternatively, take the Date object from Spire's DateTime API and build the string yourself from the standard year, month, day, hour, minute and second properties.
The downloaded docx will not open and is reported as corrupted
Cause: The Blob's MIME type is wrong, or the file is downloaded without being read back from the VFS after it is saved.
Solution: A docx needs the full MIME type declared, and the bytes that are downloaded have to be the ones read from the VFS after SaveToFile:
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const modifiedFile = new Blob([modifiedFileArray], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
});
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.
Set Word Merge Field Switches and Conditional Fields with JavaScript in React
Merge fields have two ways of changing the output without changing the replacement step, and both are easy to overlook.
One is the field switch: codes such as \b, \f and \* Upper that follow the field name inside the field code, and make the same value land in the document in a different shape — with a prefix, a suffix, or converted to upper case. A switch is part of the template, so editing the template edits the behaviour.
The other is the conditional field: the document hard-codes no sentence at all, but carries an IF field that lets Word decide, when the document is saved, which sentence the current data calls for. It is assembled by code on the spot.
Spire.Doc for JavaScript builds both entirely in the browser via WebAssembly, using a virtual file system (VFS) to manage the template document — 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.
Use Field Switches in a Merge Field
Prepare the Template
To lay the output of every switch out side by side for comparison, the template makes the same field, XX_Name, appear nine times, each occurrence carrying a different switch:
Contact
Default: «XX_Name»
\b "Mr ": «XX_Name»
\b Mr: «XX_Name»
\f " (Author)": «XX_Name»
\* Upper: «XX_Name»
\* Lower: «XX_Name»
\* FirstCap: «XX_Name»
\* Caps: «XX_Name»
\* MERGEFORMAT: «XX_Name»
With the field codes shown, each line in Word looks roughly like this ({ } are Word's field braces, which you type with Ctrl + F9):
{ MERGEFIELD XX_Name \b "Mr " \* MERGEFORMAT }
{ MERGEFIELD XX_Name \b Mr \* MERGEFORMAT }
{ MERGEFIELD XX_Name \f " (Author)" \* MERGEFORMAT }
{ MERGEFIELD XX_Name \* Upper \* MERGEFORMAT }
…
Spaces separate the field name from the switch and the switches from one another. If the text argument of a switch itself contains a space, wrap it in double quotes (\b "Mr ") — in that case the space inside the quotes is part of the argument.
The Code
function App() {
const MailMergeSwitches = 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 = "MailMergeSwitches.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document and load the template
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Provide a value for a single field; all nine occurrences use it
let fieldNames = ["XX_Name"];
let fieldValues = ["jack tang"];
// Execute the mail merge
doc.MailMerge.Execute(fieldNames, fieldValues);
// Define the output file name
const outputFileName = "MailMergeSwitches-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 into a Word template that carries field switches</h1>
<button onClick={MailMergeSwitches}>Generate</button>
</div>
);
}
export default App;
There is no switch-related API on the code side — every switch lives in the template's field codes, and Execute only has to hand XX_Name a value. That is exactly what a field switch is for: the data stays the same and the template decides the shape.
The value here is deliberately written in lower case, as "jack tang". Passing "Jack Tang" would make the output of the case-related switches look exactly like the default line, so the difference would not be visible.
The Result
| What the template says | Output |
|---|---|
«XX_Name» |
jack tang |
«XX_Name \b "Mr "» |
Mr jack tang |
«XX_Name \b Mr» |
Mrjack tang |
«XX_Name \f " (Author)"» |
jack tang (Author) |
«XX_Name \* Upper» |
JACK TANG |
«XX_Name \* Lower» |
jack tang |
«XX_Name \* FirstCap» |
Jack tang |
«XX_Name \* Caps» |
Jack Tang |
«XX_Name \* MERGEFORMAT» |
jack tang |
A few points worth noting:
- The difference between
\b "Mr "and\b Mris that pair of quotes. The argument of the first one isMrwith a trailing space, which outputsMr jack tang; the argument of the second isMrwith no space, so it sits flush against the value and becomesMrjack tang. To leave a gap between the added text and the working text, either put the space inside the quotes or write the argument as\b "Mr ". \fis a suffix and\bis a prefix, and both can appear in the same field.\* FirstCapcapitalises only the first character (Jack tang), while\* Capscapitalises the first letter of every word (Jack Tang). The two switches have very similar names but different effects.\* MERGEFORMATdoes not affect the text. It preserves the field's formatting, Word adds it by default when it creates a merge field, and leaving it in does no harm.
Build and Execute a Conditional Field
The Approach
The structure of a conditional field (an IF field) is "one field, holding a nested merge field as the test, followed by two result texts":
{ IF { MERGEFIELD Count } > "1" "Greater than one" "Less than one" }
Taken apart, this stretch contains four kinds of element:
- an
IfField, whose field type is set toFieldType.FieldIf; - a merge field node (
FieldType.FieldMergeField) — the value being compared in the condition; - the operator and the two branch texts, appended as ordinary text fragments;
- a
FieldMarkof typeFieldMarkType.FieldEnd, which closes the field off — it also has to be assigned back toIfField.Endfor the field to be complete.
The Code
This document has no input template at all; both IF fields are built from scratch:
function App() {
const ExecuteConditionalField = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the ARIALUNI.TTF font into the virtual file system (VFS)
await window.spire.FetchFileToVFS('ARIALUNI.TTF','/Library/Fonts/',`${process.env.PUBLIC_URL}/static/font/`);
// Create a document and add one section with two paragraphs
let doc = new docModule.Document();
let section = doc.AddSection();
let paragraph = section.AddParagraph();
let ifField1 = new docModule.IfField(doc);
ifField1.Type = docModule.FieldType.FieldIf;
ifField1.Code = "IF ";
paragraph.Items.Add(ifField1);
paragraph.AppendField("Count", docModule.FieldType.FieldMergeField);
paragraph.AppendText(" > ");
paragraph.AppendText("\"1\" ");
paragraph.AppendText("\"Greater than one\" ");
paragraph.AppendText("\"Less than one\"");
let end1 = doc.CreateParagraphItem(docModule.ParagraphItemType.FieldMark);
end1.Type = docModule.FieldMarkType.FieldEnd;
paragraph.Items.Add(end1);
ifField1.End = end1;
paragraph = section.AddParagraph();
let ifField2 = new docModule.IfField(doc);
ifField2.Type = docModule.FieldType.FieldIf;
ifField2.Code = "IF ";
paragraph.Items.Add(ifField2);
paragraph.AppendField("Age", docModule.FieldType.FieldMergeField);
paragraph.AppendText(" > ");
paragraph.AppendText("\"50\" ");
paragraph.AppendText("\"The old man\" ");
paragraph.AppendText("\"The young man\"");
let end2 = doc.CreateParagraphItem(docModule.ParagraphItemType.FieldMark);
end2.Type = docModule.FieldMarkType.FieldEnd;
paragraph.Items.Add(end2);
ifField2.End = end2;
// Provide values for the merge fields inside the conditional fields
let fieldNames = ["Count", "Age"];
let fieldValues = ["2", "30"];
doc.MailMerge.Execute(fieldNames, fieldValues);
// Make the fields evaluate when the document is saved
doc.IsUpdateFields = true;
// Define the output file name
const outputFileName = "ExecuteConditionalField-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>Create and execute a conditional field</h1>
<button onClick={ExecuteConditionalField}>Generate</button>
</div>
);
}
export default App;
A few details that have to happen in this order:
ifField.Code = "IF "only marks the field type. What actually assembles the body of the field is theAppendFieldand theAppendTextcalls that follow — they are laid out into this field one after another.- The operator and the two branch texts are all appended with
AppendText, and the quotes among them have to be written into the string itself ("\"1\" "). An IF field requires both result texts to be quoted, and these quote characters are what does it. - The order is fixed: compared value → operator → comparison basis → true branch → false branch. The false branch needs no extra space after its closing quote.
FieldEndis the field's closing mark. It has to be created withdocument.CreateParagraphItem(wasmModule.ParagraphItemType.FieldMark), assigned toend.Type, appended to the paragraph, and then given toifField.End. Skip any one of those steps and the field is incomplete.document.IsUpdateFields = truedecides whether the field is evaluated when the document is saved — see below.
The Result
With Count = "2" and Age = "30", the two conditional fields take the true branch and the false branch respectively:
Greater than one
The young man
Swapping the values flips the result, without touching either the template or the code:
| Field values | First paragraph (IF Count > "1") |
Second paragraph (IF Age > "50") |
|---|---|---|
Count = "2", Age = "30" |
Greater than one |
The young man |
Count = "0", Age = "70" |
Less than one |
The old man |
FAQ
The conditional field saves as two blank paragraphs
Cause: document.IsUpdateFields = true was not set. A field in the document is only an instruction waiting to be evaluated; without that switch turned on it is not executed when the document is saved, and neither branch's text is output — even the text of the false branch disappears along with it, which is why the result looks empty.
Solution: Between Execute and SaveToFile, add:
document.MailMerge.Execute(fieldNames, fieldValues);
document.IsUpdateFields = true; // Make the fields evaluate when the document is saved
document.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
The prefix added by \b is stuck to the name
Cause: the text argument of a switch carries no space of its own. The argument of \b Mr is the three characters Mr, and the spacing around it in the output is decided entirely by the argument itself.
Solution: to leave a gap between the name and the prefix, write the space into the argument and wrap it in quotes:
{ MERGEFIELD XX_Name \b "Mr " \* MERGEFORMAT }
\f works the same way. The reverse also works — leave the space outside the field by having it followed by a space in Word — but that is paragraph text, a different thing from a switch.
What is the difference between \* FirstCap and \* Caps
\* FirstCap converts only the first character to upper case and leaves the rest alone: jack tang → Jack tang.
\* Caps converts the first letter of every word to upper case: jack tang → Jack Tang.
To see the difference between these two switches, the value behind the field name has to be lower case or mixed case; if the value is already Jack Tang, both lines come out exactly like the default line.
Some switches have no effect at all
Not every Word field switch takes part in the text replacement of a mail merge. \* MERGEFORMAT preserves the field's formatting and never changes the text to begin with; switches such as \m, \v and \# 0.00 (a number format) have no visible effect either when used on a merge field with a plain text value like the one here. In practice, it is best to stick to the ones verified in this article: \b, \f, \* Upper, \* Lower, \* FirstCap and \* Caps.
How are the spaces between the field name, switches and arguments counted
- Between the field name and the first switch: at least one space;
- Between switches: at least one space;
- For a switch's text argument: a space is needed between it and the switch letter too (
\bandMr), and once the argument itself contains a space or a special character, wrap it in double quotes.
Extra spaces do not affect parsing, but leaving one out lets the argument be swallowed by the previous token — write \* Upper as \*Upper, for example, and on some parsing paths the switch stops working.
Changing the values of Count and Age does not change the output
Cause: the result text of a conditional field is cached in the document and is only evaluated again while IsUpdateFields is on. At the same time, check that the field names handed to Execute match the names written in AppendField (the comparison is case-sensitive).
Solution: confirm that fieldNames matches the first argument of AppendField exactly, and keep IsUpdateFields = true.
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.
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:
- Hide empty paragraphs and regions after the merge
- Read the region names and the merge field name list
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:
«Optional Note»occupies a paragraph of its own — after the merge not a single character is left inside it;- 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 aMERGEFIELDfield in Word just like any other; only the field name is written asGroupStart:region name/GroupEnd:region name, and without quotation marks; Note: «Optional Remark»— the field and the label share one paragraph, so the line still holds the labelNote: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 Remarkis just as empty, but because the same paragraph still holds the labelNote:, 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. HideEmptyParagraphsis the property that actually deletes. SettingHideEmptyGrouptotrueon its own while leavingHideEmptyParagraphs = falseproduces 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 onHideEmptyParagraphsis enough.
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 atext/plainBlob triggers the download. The sample finishes withdocument.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:Productsis aMERGEFIELDfield 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,ProductsandTeamRankingeach appear twice. To get the real field names, either read region by region with thegroupNameversion, or filter the region names out of the full list.- The
groupNameversion returns only the real field names inside that region — none of the six results forProductscontains aGroupStart:orGroupEnd: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, andcontent.join("\n")adds another one between the elements, so two newlines end up in a row. For a compact list, drop the"\n"from thepushcall:
content.push(GroupNames[i]);
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.