Generating Word Documents in Salesforce Lightning Web Components (LWC) Using docx.js

Generating Word Documents in Salesforce Lightning Web Components (LWC) Using docx.js

Introduction

In today’s digital era, document generation is an essential feature for many applications, including Salesforce. The ability to dynamically create Word documents can significantly enhance user experience. This includes exporting reports. It also involves generating invoices and creating custom templates. This blog explores how to generate Word documents using the docx.js library in a Lightning Web Component (LWC). We will dive into a practical example. It involves creating a document containing account and contact information. The document will include custom styles, tables, headers, and footers.

What is docx.js?

Docx.js is a JavaScript library for creating Word documents programmatically. It allows developers to define content, styles, tables, headers, and other elements using a structured approach. By leveraging docx.js, you can dynamically generate Word documents in client-side applications, such as Salesforce Lightning Web Components.

Practical Use Case in Lightning Web Components

In Salesforce, a common requirement is to generate account-related documents that include account details, associated contacts, and other customized sections. Using LWC and docx.js, you can create professional, branded documents without relying on server-side tools or third-party integrations.


Example: Building a Dynamic Document Generator

Below is an example of an LWC component. It fetches account and contact data using Apex. It generates a Word document with multiple sections and examples.

Download Doc.js library from here and add to static resources.

Key Features
  • Account Details: Displayed with a heading and introductory paragraph.
  • Contact Information: Two types of tables (simple and detailed).
  • Document Examples: Includes lists, headers, footers, and even bi-directional text support.
  • Table of Contents: Automatically generated for easy navigation.
Code Breakdown

Here’s the structure of the LWC component:

  1. Library Initialization:
    • Load the docx.js library using loadScript.
    • Ensure the library is initialized before document generation.
  2. Data Fetching:
    • Retrieve account and related contacts using Apex methods.
  3. Document Structure:
    • Define styles (e.g., headings, paragraph formats).
    • Add sections for tables, lists, and other elements.
  4. Document Generation:
    • Assemble the document with the fetched data and predefined styles.
    • On Click of download button , Generate the .docx file and download the same in browser.

Demo –

Code –

/**
* ContactGrabber Apex Class
* This class provides methods to retrieve related contacts for an account,
* and fetch account details.
*/
public with sharing class ContactGrabber {
/**
* Retrieves all contacts related to a given account.
* @param acctId The Id of the account for which to retrieve related contacts.
* @return A list of Contact records related to the specified account.
*/
@AuraEnabled(cacheable=true)
public static List<Contact> getAllRelatedContacts(Id acctId) {
return [SELECT Id, FirstName, LastName, Email, Phone FROM Contact WHERE AccountId = :acctId];
}
/**
* Retrieves details of an account including its name, description, industry,
* annual revenue, phone number, and the first attachment as a base64 encoded string.
* @param acctId The Id of the account to retrieve details for.
* @return A map containing account details and the first attachment in base64 format.
*/
@AuraEnabled(cacheable=true)
public static Map<String, Object> getAccountDetails(Id acctId) {
// Query account details
Account account = [SELECT Name, Description, Industry, AnnualRevenue, Phone FROM Account WHERE Id = :acctId LIMIT 1];
// Initialize response map
Map<String, Object> accountDetails = new Map<String, Object>();
accountDetails.put('Name', account.Name);
accountDetails.put('Description', account.Description);
accountDetails.put('Industry', account.Industry);
accountDetails.put('AnnualRevenue', account.AnnualRevenue);
accountDetails.put('Phone', account.Phone);
// Query the first attachment separately
List<Attachment> attachments = [SELECT Id, Body FROM Attachment WHERE ParentId = :acctId LIMIT 1];
if (!attachments.isEmpty()) {
accountDetails.put('ImageBase64', 'data:image/jpeg;base64,' + EncodingUtil.base64Encode(attachments[0].Body));
}
System.debug('accountDetails+++'+accountDetails);
return accountDetails;
}
}
<!–wordDocGenerator.html–>
<template>
<lightning-card title="Word Document Generator" icon-name="standard:document">
<div class="slds-p-bottom_x-large">
<lightning-button class="hidden slds-float_left slds-p-right_medium" style="padding: 10px;" variant="brand"
onclick={startDocumentGeneration} label="Download Document"></lightning-button>
</div>
</lightning-card>
</template>
/*wordDocGenerator.js*/
// import necessary modules and Apex methods
import { LightningElement, api } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import docxImport from '@salesforce/resourceUrl/docxjs';
import getAllRelatedContacts from '@salesforce/apex/ContactGrabber.getAllRelatedContacts';
import getAccountDetails from '@salesforce/apex/ContactGrabber.getAccountDetails';
export default class WordDocGenerator extends LightningElement {
// recordId of the Account to generate the document for
@api recordId;
// URL to download the generated .docx file
downloadURL;
// Flag to check if docx.js is loaded
docxInitialized = false;
// No border style for table cells
_noBorder = {
top: { style: 'none', size: 0, color: 'FFFFFF' },
bottom: { style: 'none', size: 0, color: 'FFFFFF' },
left: { style: 'none', size: 0, color: 'FFFFFF' },
right: { style: 'none', size: 0, color: 'FFFFFF' }
};
/**
* @description ConnectedCallback Lifecycle is called automatically by the Lightning Web Component framework when the component is initialized.
* It uses the `loadScript` function to load the docx.js library from a static resource.
* If the library loads successfully, it sets the `docxInitialized` flag to true and calls `renderButtons` to unhide the controls.
* If there is an error loading the library, it logs the error to the console.
* @returns {Promise<void>} A promise that resolves when the script is loaded.
*/
async connectedCallback() {
try {
// Load the docx.js library
await loadScript(this, docxImport);
this.docxInitialized = true;
this.renderButtons();
} catch (error) {
console.error('Error loading docx library:', error);
}
}
/**
* @description renderButtons method renders the buttons by removing the 'hidden' class from elements with the 'hidden' class.
* This method is called after the docx.js library is successfully loaded.
* It ensures that the buttons for generating the document are visible to the user.
* @returns {void}
*/
renderButtons() {
const elems = this.template.querySelectorAll('.hidden');
elems.forEach((e) => e.classList.remove('hidden'));
}
/**
* @description startDocumentGeneration method starts the document generation process by fetching related contacts and account details using Apex methods.
* It first checks if the docx.js library is loaded. If not, it logs an error message.
* @returns {void}
*/
startDocumentGeneration() {
// Check if docx.js is loaded
if (!this.docxInitialized) {
// If not loaded, log an error and return
console.error('docx.js is not loaded yet.');
return;
}
// Call Apex methods to get related contacts and account details
// Use Promise.all to fetch both concurrently
Promise.all([
getAllRelatedContacts({ acctId: this.recordId }),
getAccountDetails({ acctId: this.recordId })
])
// Handle the results of both Apex calls
.then(([contacts, accounts]) => {
// build the Word document with the fetched data
this.buildDocument(contacts, accounts);
})
// Handle any errors that occur during the Apex calls
.catch((err) => {
console.error('Error fetching data from Apex:', err);
});
}
/**
* @description buildDocument method constructs a Word document using the docx.js library.
* It creates a document with custom styles, a table of contents, and various sections including account details and contact information.
* The document includes:
* – A table of contents on the first page
* – An account details section with contact tables (2 fields and 4 fields)
* – An account image if available
* – Examples of numbered lists (Roman and Decimal)
* – A section with a header and footer containing page numbers
* – Right-to-left text examples
* – A paragraph split across two pages
* – A table split across two pages
* – A new example of a table with many rows that spans multiple pages, with a repeating header
* @param {Array} contacts – An array of contact objects related to the account.
* @param {Array} account – An object containing account details.
* @returns {void}
*/
buildDocument(contacts, account) {
// Intialize docx.js classes which will be used to create the document
const {
Document,
Paragraph,
TextRun,
HeadingLevel,
AlignmentType,
Table,
TableRow,
TableCell,
TableOfContents,
StyleLevel,
Media,
Header,
Footer,
PageNumber,
PageNumberFormat,
WidthType
} = window.docx;
// 1) Create a new Document instance with custom styles and numbering configurations
// Define styles for headings, paragraphs, and lists
const doc = new Document({
styles: {
// Define the heading and paragraph styles
paragraphStyles: [
// Heading1 style with custom font, size, underline, alignment, and spacing
{
id: 'Heading1',
name: 'Heading 1',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: {
font: 'Calibri',
size: 52,
bold: true,
color: '000000',
underline: { type: window.docx.UnderlineType.SINGLE, color: '000000' }
},
paragraph: { alignment: AlignmentType.CENTER, spacing: { line: 340 } }
},
// Heading2 style with custom font, size, bold, and spacing
{
id: 'Heading2',
name: 'Heading 2',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: { font: 'Calibri', size: 26, bold: true },
paragraph: { spacing: { line: 340 } }
},
// Normal paragraph style with custom font, size, bold and spacing like line height, before and after spacing
// Tab stops positions like rightTabStop and leftTabStop
{
id: 'NormalParaBold',
name: 'Normal Para Bold',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: { font: 'Calibri', size: 26, bold: true },
paragraph: {
spacing: { line: 276, before: 20 * 72 * 0.1, after: 20 * 72 * 0.05 },
rightTabStop: window.docx.TabStopPosition.MAX,
leftTabStop: 453.543307087
}
},
// Normal paragraph style with custom font, size, alignment and spacing like line height, before and after spacing
{
id: 'NormalPara',
name: 'Normal Para',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: { font: 'Calibri', size: 26 },
paragraph: {
alignment: AlignmentType.JUSTIFIED,
spacing: { line: 276, before: 20 * 72 * 0.1, after: 20 * 72 * 0.05 }
}
},
// List Paragraph style with custom font, size, and quick format
{
id: 'ListParagraph',
name: 'List Paragraph',
basedOn: 'Normal',
quickFormat: true
}
]
},
// Define numbering configurations and styles for Roman and Decimal
numbering: {
// Configurations for Roman and Decimal styles
config: [
{
// Roman numbering configuration
reference: 'roman-numbering',
// Levels for Roman numbering
levels: [
{
level: 0, // Level 0 for Roman numbering
format: 'upperRoman', // Use uppercase Roman numerals
text: '%1', // Placeholder for numbering text
alignment: AlignmentType.START, // Align to start
style: {
// Paragraph style for Roman numbering
paragraph: { indent: { left: 720, hanging: 260 } }
}
}
]
},
{
// Decimal numbering configuration
reference: 'decimal-numbering',
// Levels for Decimal numbering
levels: [
{
level: 0, // Level 0 for Decimal numbering
format: 'decimal', // Use decimal numbering
text: '%1', // Placeholder for numbering text
alignment: AlignmentType.START, // Align to start
style: {
// Paragraph style for Decimal numbering
paragraph: { indent: { left: 720, hanging: 260 } }
}
}
]
}
]
}
});
// 2) Add a custom heading for the document
// Example of using heading level 1 with custom text and alignment
// Defined Table of Contents Heading
const introHeading2 = new Paragraph({
text: 'Table of Contents',
heading: HeadingLevel.HEADING_1,
alignment: AlignmentType.CENTER,
spacing: { after: 200 }
});
// 2a) Create a Table of Contents with custom styles and hyperlinking
const toc = new TableOfContents('Table of Contents', {
hyperlink: true,
headingStyleRange: '1-5',
stylesWithLevels: [new StyleLevel('Heading1', 1), new StyleLevel('Heading2', 2)]
});
// Add the Table of Contents to the document
doc.addSection({
children: [introHeading2,toc]
});
// 3) Add a section for Account Details and Contacts
// Example of using heading level 2 with account name fetched from Apex
const introHeading = new Paragraph({
text: `Account Details: ${account.Name}`,
heading: HeadingLevel.HEADING_2,
alignment: AlignmentType.CENTER,
spacing: { after: 200 }
});
// 3a) Example of a paragraph with a text run containing additional information
const introPara = new Paragraph({
children: [
// Add a text run with custom text and formatting
new TextRun('This document contains all relevant account and contact information.'),
// Add a bold text run with a tab character
new TextRun({ text: '\tGenerated with LWC + docx.js.', bold: true })
],
// Set the style for the paragraph
style: 'NormalPara'
});
// 3b) Build a 2-column contact table (First Name / Last Name)
// Example of using a table with two columns for contact information
const twoFieldRows = [];
twoFieldRows.push(
new TableRow({
tableHeader: true,
children: [
new TableCell({
children: [new Paragraph('First Name')],
borders: this._noBorder,
style: 'NormalParaBold'
}),
new TableCell({
children: [new Paragraph('Last Name')],
borders: this._noBorder
})
]
})
);
// Loop through the contacts array to create table rows
contacts.forEach((ct) => {
twoFieldRows.push(
new TableRow({
children: [
new TableCell({
// Create a paragraph with a text run for the first name
children: [new Paragraph({ children: [this._createTextRun(ct.FirstName)] })]
}),
new TableCell({
// Create a paragraph with a text run for the last name
children: [new Paragraph({ children: [this._createTextRun(ct.LastName)] })]
})
]
})
);
});
// Create the table with the defined rows and set its width to 100% of the page
const twoFieldTable = new Table({
width: { size: 100, type: WidthType.PERCENTAGE }, // full page width
rows: twoFieldRows, // set the rows to twoFieldRows defined above
});
// 3c) Build a 4-column contact table (First Name / Last Name / Email / Phone)
// Example of using a table with four columns for contact information
const fourFieldRows = [];
fourFieldRows.push(
new TableRow({
tableHeader: true,
children: [
new TableCell({
// Create a paragraph with a text run for the first name header
children: [new Paragraph('First Name')],
// Set no borders for the cell
borders: this._noBorder
}),
new TableCell({
// Create a paragraph with a text run for the last name header
children: [new Paragraph('Last Name')],
// Set no borders for the cell
borders: this._noBorder
}),
new TableCell({
// Create a paragraph with a text run for the email header
children: [new Paragraph('Email')],
// Set no borders for the cell
borders: this._noBorder
}),
new TableCell({
// Create a paragraph with a text run for the phone header
children: [new Paragraph('Phone')],
// Set no borders for the cell
borders: this._noBorder
})
]
})
);
// Loop through the contacts array to create table rows
contacts.forEach((ct) => {
fourFieldRows.push(
new TableRow({
children: [
new TableCell({
children: [new Paragraph({ children: [this._createTextRun(ct.FirstName)] })]
}),
new TableCell({
children: [new Paragraph({ children: [this._createTextRun(ct.LastName)] })]
}),
new TableCell({
children: [new Paragraph({ children: [this._createTextRun(ct.Email)] })]
}),
new TableCell({
children: [new Paragraph({ children: [this._createTextRun(ct.Phone)] })]
})
]
})
);
});
// Create the table with the defined rows and set its width to 100% of the page
const fourFieldTable = new Table({
width: { size: 100, type: WidthType.PERCENTAGE }, // full page width
rows: fourFieldRows, // set the rows to fourFieldRows defined above
});
// 3d) Add Image from Account (if available)
// Example of adding an image to the document from account attachments.
// Define a paragraph to hold the image if it exists
let imagePara;
// Check if the account has an image in Base64 format
if (account.ImageBase64) {
// If the image exists, convert the Base64 string to an ArrayBuffer
let base64Str = account.ImageBase64;
// If the Base64 string starts with 'data:image', remove the prefix
if (base64Str.startsWith('data:image')) {
// Remove the data URL prefix to get the raw Base64 string
base64Str = base64Str.split(',')[1];
}
// Remove any whitespace characters from the Base64 string
base64Str = base64Str.replace(/\s/g, '');
// Convert the Base64 string to an ArrayBuffer
const imgBuffer = this._base64ToArrayBuffer(base64Str);
// Add the image to the document using Media.addImage and set its dimensions as 600 height and 300 width
const insertedImg = Media.addImage(doc, imgBuffer, 600, 300);
// Create a paragraph to hold the inserted image with spacing after it
imagePara = new Paragraph({ children: [insertedImg],spacing: { after: 800 }, });
}
// 3e) Add this “Account + Contacts” section (starts on page 2)
// Create a new section for the account and contacts information
const sectionAccountChildren = [
// Add the intro heading and paragraph to the section
introHeading,
// Add the intro paragraph with custom text runs
introPara,
// Add the two-field and four-field contact tables to the section
new Paragraph({ text: 'Contacts List (2 fields)', style: 'NormalParaBold', spacing: { after: 100 }, }),
// Add the two-field table to the section
twoFieldTable,
// Add a paragraph with spacing after it, to put some space between the table and next content
new Paragraph({ text: '', style: 'NormalParaBold', spacing: { after: 100 }, }),
// Add a heading for the four-field contacts list
new Paragraph({ text: 'Contacts List (4 fields)', heading: HeadingLevel.HEADING_2, spacing: { after: 100 }, }),
// Add the four-field table to the section
fourFieldTable,
// Add a paragraph with spacing after it, to put some space between the table and next content
new Paragraph({ text: '', style: 'NormalParaBold',spacing: { after: 100 }, }),
];
// If an image was added, include it in the section
if (imagePara) {
// Add a heading for the account image and the image paragraph
sectionAccountChildren.push(new Paragraph({ text: 'Account Image', heading: HeadingLevel.HEADING_2, spacing: { after: 400 } }));
// Add the image paragraph to the section
sectionAccountChildren.push(imagePara);
}
// Add the section to the document with the account details and contacts
doc.addSection({
children: sectionAccountChildren
});
// 4) Example 1: Roman-Numbered List (each example on its own page)
doc.addSection({
children: [
// Add a heading for the Roman-numbered list example
new Paragraph({ text: 'Example: Roman-Numbered List', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
// Add paragraphs with Roman numbering for each item
new Paragraph({
text: 'Item I',
numbering: { reference: 'roman-numbering', level: 0 }
}),
// Add another item with Roman numbering
new Paragraph({
text: 'Item II',
numbering: { reference: 'roman-numbering', level: 0 }
})
]
});
// 5) Example 2: Decimal-Numbered List (on its own page)
doc.addSection({
children: [
// Add a heading for the Decimal-numbered list example
new Paragraph({ text: 'Example: Decimal-Numbered List', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
// Add paragraphs with Decimal numbering for each step
new Paragraph({
text: 'Step 1 — Gather ingredients',
numbering: { reference: 'decimal-numbering', level: 0 }
}),
// Add another step with Decimal numbering
new Paragraph({
text: 'Step 2 — Mix ingredients',
numbering: { reference: 'decimal-numbering', level: 0 }
}),
// Add another step with Decimal numbering
new Paragraph({
text: 'Step 3 — Bake at 350°F',
numbering: { reference: 'decimal-numbering', level: 0 }
})
]
});
// 6) Example 3: Header & Footer with Page Numbers (on its own page)
doc.addSection({
// Set the header for the section
headers: {
// Default header for the section
default: new Header({
children: [
// Add a paragraph with the company name and current page number
new Paragraph({
children: [
// Add a text run with the company name
new TextRun('My Company'),
// Add a text run with the current page number
new TextRun({ children: [' – Page ', PageNumber.CURRENT] })
]
})
]
})
},
// Set the footer for the section
footers: {
default: new Footer({
children: [
// Add a centered paragraph with the text and page numbers
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
// Add a text run
new TextRun('© Confidential'),
// Add a text run with the current page number and total pages
new TextRun({ children: [' | Page ', PageNumber.CURRENT, ' of ', PageNumber.TOTAL_PAGES] })
]
})
]
})
},
// Set the page numbering format and start from page 1
properties: {
pageNumberStart: 1,
pageNumberFormatType: PageNumberFormat.DECIMAL
},
children: [
// Add a heading for the header/footer example
new Paragraph({ text: 'Example: Header/Footer', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
// Add a paragraph with a description of the header/footer example
new Paragraph({ text: 'This section demonstrates a custom header and footer with dynamic page numbers.' })
]
});
// 7) Example 4: Right-to-Left (BiDi) Text (on its own page)
doc.addSection({
children: [
// Add a heading for the Right-to-Left text example
new Paragraph({ text: 'Example: Right-to-Left Text', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
// Add paragraphs with Right-to-Left text runs
new Paragraph({
// Set the paragraph to be bidirectional (RTL)
bidirectional: true,
children: [
// Add a text run with Right-to-Left text and bold formatting
new TextRun({ text: 'hello world', rightToLeft: true, bold: true })
]
}),
new Paragraph({
// Set the paragraph to be bidirectional (RTL)
bidirectional: true,
children: [
// Add a text run with Right-to-Left text and italics formatting
new TextRun({ text: 'This is text in the right-to-left border', italics: true, rightToLeft: true })
]
})
]
});
// 8) Example 5: Paragraph Split Across Two Pages
// 8a) Create the first part of a long paragraph that will be split across two pages
const longTextPart1 = new Paragraph({
// Add a long paragraph that will be split across two pages
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum. Cras venenatis euismod malesuada.',
// Use the NormalPara style defined earlier
style: 'NormalPara'
});
// 8b) Create a second part of the long paragraph that will continue on the next page
const longTextPart2 = new Paragraph({
// Add a continuation of the long paragraph
text:
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
// Use the NormalPara style defined earlier
style: 'NormalPara',
// Set page break before this paragraph to ensure it starts on a new page
// This will ensure that the second part of the paragraph starts on a new page
// when the first part is too long to fit on the current page
pageBreakBefore: true
});
// Add the section with the split paragraph to the document
doc.addSection({
children: [
// Add a heading for the paragraph split example
new Paragraph({ text: 'Example: Paragraph Split Across Pages', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
// Add the first part of the long paragraph
longTextPart1,
// Add a paragraph with a description of the split paragraph example
longTextPart2
]
});
// 9) Example 6: Table Split Across Two Pages
// 9a) First half of the table
// Example of using a table with two columns and multiple rows
const tablePart1Rows = [];
tablePart1Rows.push(
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Header A')], borders: this._noBorder }),
new TableCell({ children: [new Paragraph('Header B')], borders: this._noBorder })
]
})
);
tablePart1Rows.push(
new TableRow({
children: [
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 1 – A')] })] }),
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 1 – B')] })] })
]
})
);
tablePart1Rows.push(
new TableRow({
children: [
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 2 – A')] })] }),
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 2 – B')] })] })
]
})
);
const tablePart1 = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: tablePart1Rows
});
// 9b) Second half of the table
const tablePart2Rows = [];
tablePart2Rows.push(
new TableRow({
children: [
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 3 – A')] })] }),
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 3 – B')] })] })
]
})
);
tablePart2Rows.push(
new TableRow({
children: [
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 4 – A')] })] }),
new TableCell({ children: [new Paragraph({ children: [this._createTextRun('Row 4 – B')] })] })
]
})
);
const tablePart2 = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: tablePart2Rows
});
doc.addSection({
children: [
new Paragraph({ text: 'Example: Table Split Across Pages', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
tablePart1,
new Paragraph({ text: '', pageBreakBefore: true }),
tablePart2
]
});
// 10) New Example 7: Table Content Split Across Pages with Repeating Header
// Build a table with many rows so it spans multiple pages.
// Use `tableHeader: true` on the first row to repeat it.
const manyRows = [];
// Header row with tableHeader: true
manyRows.push(
new TableRow({
tableHeader: true,
children: [
new TableCell({
children: [new Paragraph('Item')],
borders: this._noBorder
}),
new TableCell({
children: [new Paragraph('Description')],
borders: this._noBorder
})
]
})
);
// Example data rows (e.g., 100 rows)
for (let i = 1; i <= 100; i++) {
manyRows.push(
new TableRow({
children: [
new TableCell({
children: [
new Paragraph({
children: [this._createTextRun(`Item ${i}`)]
})
]
}),
new TableCell({
children: [
new Paragraph({
children: [this._createTextRun(`Description for item ${i}, which may be long enough to wrap or push rows to the next page.`)]
})
]
})
]
})
);
}
const manyRowsTable = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: manyRows
});
doc.addSection({
children: [
new Paragraph({ text: 'Example: Table Content Split Across Pages', heading: HeadingLevel.HEADING_1, pageBreakBefore: true }),
new Paragraph({ text: 'Below is a table whose content spans multiple pages. Notice how the header repeats on each new page.' }),
manyRowsTable
]
});
// 11) Pack the document and produce a downloadable URL
window.docx.Packer.toBase64String(doc)
.then((b64) => {
// Set the download URL to be used for downloading the document
// The URL is prefixed with the MIME type for Word documents
// This allows the browser to recognize it as a downloadable file
// and handle it accordingly when the user clicks the download link
this.downloadURL =
'data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,' + b64;
const dlLink = this.template.querySelector('.slds-hide');
if (dlLink) {
dlLink.classList.remove('slds-hide');
}
// Create an <a> element, click it to download immediately
const link = document.createElement('a');
// Set the name of the account to be used in the file name and replace any invalid characters
const safeName = account.Name.replace(/[^a-z0-9_\-]/gi, '_').toLowerCase();
// Set the href and download attributes for the link
link.href = this.downloadURL;
// Set the download attribute to specify the file name
link.download = `${safeName}_Details.docx`;
// Append the link to the body, click it to trigger the download, and then remove it
document.body.appendChild(link);
// Trigger the download by simulating a click on the link
link.click();
// Remove the link from the document after the download
document.body.removeChild(link);
})
.catch((err) => {
console.error('Error generating .docx:', err);
});
}
/**
* @description Helper method to create a TextRun with custom formatting.
* @param {string} text – The text to be included in the TextRun.
* @returns {TextRun} – A TextRun object with the specified text and formatting.
*/
_createTextRun(text) {
return new window.docx.TextRun({
text,
bold: true,
size: 20,
font: 'Calibri'
});
}
/**
* @description Helper method to convert a Base64 string to an ArrayBuffer.
* This is used to convert images from Base64 format to a format that can be used in the Word document.
* @param {string} base64 – The Base64 string to be converted.
* @returns {ArrayBuffer} – The converted ArrayBuffer.
*/
_base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
}
<?xml version="1.0"?>
<!–wordDocGenerator.js-meta.xml–>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"&gt;
<apiVersion>63.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__HomePage</target>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>

Download Docx JS Examples –

Conclusion

By integrating docx.js with Lightning Web Components, you can unlock powerful document-generation capabilities directly within Salesforce. This example showcases the flexibility of docx.js and demonstrates its practical applications in creating rich, dynamic Word documents tailored to user needs. Whether for client-facing documents or internal reports, this approach enhances productivity and streamlines workflows.

Leave a comment