By default, docxtemplater does not allow complex expressions. This means that by default, if the tag is {user.name}
, it will try to retrieve data["user.name"]
, which will return undefined
.
By enabling the feature called "angular-parser", you will be able to write :
{user.name}
: Nested property access{price + tax}
: Addition or multiplication{name | toUpperCase}
: Filters to transform your data in the template{name = "John"}
: Assignments, which are useful if you need to set data elements from the template.Make sure to use angular-expressions version 1.4.3 or higher (released on 10th December 2024), because angular-expressions 1.4.2 and below had a security issue. You can check which version is used by looking in the file node_modules/angular-expressions/package.json
and look at the "version" field.
To install angular-expressions
(which is required to enable the angular-parser), use :
npm install --save angular-expressions
Then use following code :
const expressionParser = require("docxtemplater/expressions.js");
const doc = new Docxtemplater(zip, {
parser: expressionParser,
});
doc.render(/* data */);
After doing the setup part, you can now try to see if you can access nested properties, by using following template:
And this data:
const expressionParser = require("docxtemplater/expressions.js");
const doc = new Docxtemplater(zip, {
parser: expressionParser,
});
doc.render({
user: {
name: "John",
},
});
This should render :
With the angular expression option set, you can also use conditions:
The first conditional will render the section only if there are 2 users or more.
The second conditional will render the section only if the first user has the name "John".
The angular parser handles the following operators :
Operator | Expression | Example | Result |
---|---|---|---|
ADDITION | a + b | 1+1 | 2 |
SUBSTRACTION | a - b | 2-1 | 1 |
MULTIPLICATION | a * b | 2\*2 | 4 |
TERNARIES | a ? b : c | true ? "welcome" : "nope" | "welcome" |
EQUALITY/INEQUALITY | a == 1 , a != 1 | 1 == 1 , 1 != 1 | true, false |
RELATIONAL | a > 1 , a < 1 , a >= 1 , a <= 1 | 3 > 1 , 3< 1 | true, false |
AND | a && b | true && false | false |
OR | a || b | true || false | true |
MODULO | a % b | 4%3 | 1 |
DIVISION | a / b | 4/ 2 | 2 |
ASSIGNMENTS | a = 1 | a = 1 | 1 |
OPERATOR PRECEDENCE with parenthesis | (a && b) || c | (true && false) || true | true |
EXPONENTIAL NUMBERS | 1e3 | 1e3 | 1000 |
For example, it is possible to write the following template:
With filters, it is possible to write the following template to have the resulting string be uppercased:
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.toUpperCase = function (input) {
/*
* Make sure that if your input is undefined, your
* output will be undefined as well and will not
* throw an error
*/
if (!input) {
return input;
}
return input.toUpperCase();
};
new Docxtemplater(zip, { parser: expressionParser });
doc.render({
user: {
name: "John",
},
});
More complex filters are possible, for example, if you would like to list the names of all active users. You could show the list of users that are older than 18, by writing the following code:
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.olderThan = function (users, minAge) {
/*
* Make sure that if your input is undefined, your
* output will be undefined as well and will not
* throw an error
*/
if (!users) {
return users;
}
return users.filter(function (user) {
return user.age >= minAge;
});
};
const doc = new Docxtemplater(zip, { parser: expressionParser });
doc.render({
users: [
{
name: "John",
age: 15,
},
{
name: "Mary",
age: 26,
},
],
});
And in your template,
Here are some interesting usecases for filters :
You can write some generic data filters using angular expressions inside the filter itself.
doc.render({
users: [
{
name: "John",
age: 10,
},
{
name: "Mary",
age: 20,
},
],
});
The argument inside the where filter can be any other angular expression, with ||, &&, etc
The code for this filter is extremely terse, and gives a lot of possibilities:
const expressionParser = require("docxtemplater/expressions.js");
const whereCache = {};
expressionParser.filters.where = function (input, query) {
let get;
if (whereCache[query]) {
get = whereCache[query];
} else {
get = expressionParser.compile(query);
whereCache[query] = get;
}
return input.filter(function (item) {
return get(item);
});
};
const doc = new Docxtemplater(zip, { parser: expressionParser });
doc.render(/* data */);
If your data is the following:
{
"items": [
{
"name": "Acme Computer",
"price": 1000
},
{
"name": "USB storage",
"price": 15
},
{
"name": "Mouse & Keyboard",
"price": 150
}
]
}
You might want to sort the items by price (ascending).
You could do that again with a filter, like this:
The code for this filter is:
const { sortBy } = require("lodash");
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.sortBy = function (input, ...fields) {
// In our example fields is ["price"]
/*
* Make sure that if your input is undefined, your
* output will be undefined as well and will not
* throw an error
*/
if (!input) {
return input;
}
return sortBy(input, fields);
};
It is possible to have multilevel loops (also called nested loops), if your data is the following :
doc.render({
companies: [
{
name: "EvilCorp",
users: [
{
firstName: "John",
},
{
firstName: "Mary",
},
],
},
{
name: "HeavenCorp",
users: [
{
firstName: "Jack",
},
{
firstName: "Bonnie",
},
],
},
],
});
You can loop on companies.users directly using following tag and filter :
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.loop = function (input, ...keys) {
const result = input.reduce(function (result, item) {
(item[keys[0]] || []).forEach(function (subitem) {
result.push({ ...item, ...subitem });
});
return result;
}, []);
if (keys.length === 1) {
return result;
}
keys.shift();
return expressionParser.filters.loop(result, ...keys);
};
With this code, you can also have more nesting if needed :
{#companies | loop:"users":"tasks"}{/}
If your data is the following:
{
"items": [
{
"name": "Acme Computer",
"price": 1000
},
{
"name": "Mouse & Keyboard",
"price": 150
}
]
}
And you would like to show the total price, you can write in your template:
The sumBy
is a filter that you can write like this:
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.sumBy = function (input, field) {
// In our example field is the string "price"
/*
* Make sure that if your input is undefined, your
* output will be undefined as well and will not
* throw an error
*/
if (!input) {
return input;
}
return input.reduce(function (sum, object) {
return sum + object[field];
}, 0);
};
This example is to format numbers in the format: "150.00" (2 digits of precision) If your data is the following:
{
"items": [
{
"name": "Acme Computer",
"price": 1000
},
{
"name": "Mouse & Keyboard",
"price": 150
}
]
}
And you would like to show the price with two digits of precision, you can write in your template:
The toFixed
is an angular filter that you can write like this:
const expressionParser = require("docxtemplater/expressions.js");
expressionParser.filters.toFixed = function (input, precision) {
// In our example precision is the integer 2
/*
* Make sure that if your input is undefined, your
* output will be undefined as well and will not
* throw an error
*/
if (!input) {
return input;
}
return input.toFixed(precision);
};
With the angular expression option, it is possible to assign a value to a variable directly from your template.
For example, in your template, write:
This will show in your output :
To drop the new line, use a raw tag instead, starting with a @ :
Output :
By default, variable assignments occur in the current scope. This behavior affects how your data is modified during template processing.
For example, when you write:
{#users}{lastUserName}{lastUserName = name}{/}
After the first iteration, your data structure becomes:
data = {
users: [
{
name: "John",
lastUserName: "John",
},
{
name: "Mary",
},
{
name: "Jack",
},
],
};
Notice that only the first item in the array has the lastUserName
property set.
Customizing Assignment Scope
You can modify this default behavior by customizing the scope handling:
const expressionParser = require("docxtemplater/expressions.js");
new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser: expressionParser.configure({
setIdentifier(tag, value, currentScope, scopeList) {
/*
* In our context scopeList[0] is the same as `data`
*
* - scopeList[1] will be the child scope (if it exists)
* => will be { name: "John" } for the first iteration
* - scopeList[2] will be the child child scope (if it exists)
* => in your case there is just one level loop
* - currentScope will in our case be the same as scopeList[1]
*/
const rootScope = scopeList[0];
rootScope[tag] = value;
// return true to notify to the expressionParser that we handled this assignment
return true;
},
}),
});
When using this custom setIdentifier
function, after processing all iterations, your data structure would look like:
data = {
users: [
{
name: "John",
},
{
name: "Mary",
},
{
name: "Jack",
},
],
lastUserName: "Jack", // Set at the root level with the last user's name
};
Instead of placing the value in the current scope (inside each user object), this approach assigns the value to the root data object, making it available globally.
One might need to have a condition on the $index when inside a loop.
For example, if you have two arrays of the same length and you want to loop over both of them at the same time:
doc.render({
names: ["John", "Mary"],
ages: [15, 26],
});
It is possible to preprocess or postprocess :
{name: "John"}
)For example, you could have some specific behavior applied for all HTML tags.
Say you wanted to use the following in your code, and still make this work with the HTML module :
doc.render({
htmlDescription: "Dear Edgar,\nWhat's your plan today ?",
});
In this case, the new line ("\n" character) will be ignored since in HTML, linebreaks can only be achieved by using a <br>
element, or a HTML block tag such as <p>
or <div>
.
One way to make this work together with the angularParser would be to postprocess your result.
You could do so like this :
const expressionParser = require("docxtemplater/expressions.js");
const htmlModuleNameBlock =
"pro-xml-templating/html-module-block";
const htmlModuleNameInline =
"pro-xml-templating/html-module-inline";
const htmlPptxModuleNameBlock =
"pro-xml-templating/html-pptx-module-block";
const htmlPptxModuleNameShape =
"pro-xml-templating/html-pptx-module-shape";
const htmlPptxModuleNameInline =
"pro-xml-templating/html-pptx-module-inline";
const htmlModuleTags = [
htmlModuleNameBlock,
htmlModuleNameInline,
htmlPptxModuleNameBlock,
htmlPptxModuleNameShape,
htmlPptxModuleNameInline,
];
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser(tag) {
const obj = expressionParser(tag);
return {
get(scope, context) {
let result = obj.get(scope, context);
if (
htmlModuleTags.indexOf(
context.meta.part.module
) !== -1 &&
typeof result === "string"
) {
// for all html tags, replace "\n" by `<br>`
result = result.replace(/\n/g, "<br>");
}
return result;
},
};
},
modules: [new HTMLModule(), new HTMLPptxModule()],
});
doc.render({
htmlDescription: "Dear Edgar,\nWhat's your plan today ?",
});
Similarly, it is possible to preprocess or postprocess, you could be inspired by one of these samples :
You could also use the Jexl library if you prefer their syntax.
We did not create a builtin integration with jexl, but it could work for you.
Use it like this :
const jexl = require("jexl");
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser: (tag) => {
const compiledVersion = jexl.compile(tag);
return {
get(scope /* , context */) {
return compiledVersion.evalSync(scope);
},
};
},
});
doc.render({
employees: [
{ first: "Sterling", last: "Archer", age: 36 },
{ first: "Malory", last: "Archer", age: 75 },
{ first: "Lana", last: "Kane", age: 33 },
{ first: "Cyril", last: "Figgis", age: 45 },
{ first: "Cheryl", last: "Tunt", age: 28 },
],
retireAge: 62,
});
With the following expression :
It will simply render :
With this code above, for loops, you won't be able to access data that is outside of the loop, you will have to implement something similar to what we have done for our integration with angular-expressions.
Similarly, you could also use filtrex :
const {
compileExpression,
useDotAccessOperator,
} = require("filtrex");
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser: (tag) => {
const fn = compileExpression(tag, {
customProp: useDotAccessOperator,
});
return {
get(scope /* , context */) {
return fn(scope);
},
};
},
});
doc.render({
foo: { bar: 42 },
});
Will result in
Or you could use jsonata
const jsonata = require("jsonata");
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser: (tag) => {
const expression = jsonata(tag);
return {
get(scope /* , context */) {
return expression.evaluate(scope); // returns 24
},
};
},
});
// We use renderAsync because jsonata is async
doc.renderAsync({
example: [{ value: 4 }, { value: 7 }, { value: 13 }],
});
With following template :
Gives :
If running in IE 11 or if you get the error Proxy is not defined
, you can use the following parser instead :
const expressionParser =
require("docxtemplater/expressions-ie11.js");
const doc = new Docxtemplater(zip, {
parser: expressionParser,
});
doc.render(/* data */);
If you use docxtemplater 3.32.2 or a previous version, the require("docxtemplater/expressions.js") call won't work. In this case, you can upgrade docxtemplater or copy the code found here