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:
- Library Initialization:
- Load the
docx.jslibrary usingloadScript. - Ensure the library is initialized before document generation.
- Load the
- Data Fetching:
- Retrieve account and related contacts using Apex methods.
- Document Structure:
- Define styles (e.g., headings, paragraph formats).
- Add sections for tables, lists, and other elements.
- Document Generation:
- Assemble the document with the fetched data and predefined styles.
- On Click of download button , Generate the
.docxfile 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"> | |
| <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