docs.sheetjs.com/docz/docs/03-demos/09-cloud/01-salesforce.md
2023-10-02 00:57:11 -04:00

20 KiB

title pagination_prev pagination_next
Salesforce LWC demos/local/index demos/extensions/index

import current from '/version.js'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock';

Salesforce is a suite of cloud-based software systems for Customer Relationship Management (CRM). "Lightning Web Components" (LWC) is a robust JavaScript extension platform available to Salesforce apps1.

SheetJS is a JavaScript library for reading and writing data from spreadsheets.

This demo explores the LWC scripting features in Salesforce. We'll explore how to install SheetJS scripts in Lightning Web Components and build a sample app for exporting lists to XLSX workbooks.

:::caution pass

Salesforce may change the platform in backwards-incompatible ways, so the demo may require some adjustments. The official documentation should be consulted.

:::

:::note

This demo was last tested on 2023 September 30 using Lightning API version 58.0.

:::

:::warning Telemetry

The Salesforce developer tools embed telemetry. It can be disabled by setting the environment variable SF_DISABLE_TELEMETRY to true or by running

npx @salesforce/cli config set disable-telemetry=true --global

:::

Integration Details

Lightning Web Components can load scripts stored in static resources.

Installation

The SheetJS Standalone scripts can be downloaded and added as a static resource.

:::info pass

Due to Salesforce name restrictions, the script must be renamed to sheetjs.js.

:::

Loading SheetJS

Assuming the script was renamed to sheetjs.js, the name of the resource will be sheetjs. async functions can use loadScript to fetch and load the library. The script will define the variable XLSX2

It is recommended to load the library in a callback. For example, the following @api method loads the library and exports sample data to a spreadsheet file:

import { LightningElement, api } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import sheetjs from '@salesforce/resourceUrl/sheetjs';

export default class SheetComponent extends LightningElement {
  @api async download() {
    await loadScript(this, sheetjs); // load the library
    // At this point, the library is accessible with the `XLSX` variable

    // Create worksheet
    var ws = XLSX.utils.aoa_to_sheet([
      [ "S", "h", "e", "e", "t", "J"," S" ],
      [  5 ,  4 ,  3 ,  3 ,  7 ,  9 ,  5  ]
    ]);

    // Create workbook and add worksheet
    var wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, "Data");

    // Export Data
    XLSX.writeFile(wb, "SheetForceExport.xlsx");
  }
}

Exporting Data from SF List

:::note pass

There are many different data types and APIs. This demo uses the deprecated getListUi function to pull account data.

:::

Using the LWC Wire Service, components receive data in separate events. Exports are typically generated in a separate event handler. Component state is normally used to handle the timing mismatch.

Getting Account Data

The main method to obtain data is getListUi and the key for account data is ACCOUNT_OBJECT:

import { LightningElement, wire } from 'lwc';
import { getListUi } from 'lightning/uiListApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';

// ...

export default class SheetComponent extends LightningElement {
  @wire(getListUi, {
    objectApiName: ACCOUNT_OBJECT.objectApiName,
    listViewApiName: 'AllAccounts'
  }) listInfo({ error, data }) {

    // LIST DATA AVAILABLE HERE

  };
  // ...
}

Array of Arrays

SheetJS most reliably translates "arrays of arrays", a nested array which directly maps to individual cell addresses. For example:

var data = [
  ["Name",      "Phone"],           // row 1
  ["Foo Bar",   "(555) 555-5555"],  // row 2
  ["Baz Qux",   "(555) 555-5556"]   // row 3
];

The APIs typically return nested objects, so the array must be constructed.

Salesforce Representation (click to show)

The data parameter in the callback has a deep structure. Typically one would set a property in the component and display data in a template:

  // ...
  // declare records variable in the component
  records;
  @wire(getListUi, {
    objectApiName: ACCOUNT_OBJECT.objectApiName,
    listViewApiName: 'AllAccounts'
  }) listInfo({ error, data }) {
    if (data) {
      // data.records.records is the array of interest
      this.records = data.records.records;
      this.error = undefined;
    }
  }
  // ...

The template itself would iterate across the records:

<template>
  <template if:true={records}>
    <table>
      <tr><th>Name</th><th>Phone</th></tr>
      <template for:each={records} for:item="record">
        <tr key={record.fields.Id.value}>
          <td>{record.fields.Name.value}</td>
          <td>{record.fields.Phone.value}</td>
        </tr>
      </template>
    </table>
  </template>
</template>

A suitable SheetJS array of arrays can be constructed by mapping across records:

      var headers = [ "Name", "Phone" ];
      var aoa = [headers].concat(data.records.records.map(record => [
        record.fields.Name.value,  // Name field
        record.fields.Phone.value, // Phone field
      ]));

State

This data is available in a wire service callback, but it is common to export the data in a separate API event. This flow is handled with a state variable:

export default class SheetComponent extends LightningElement {
  // highlight-next-line
  aoa; // will hold data for export
  @wire(getListUi, {
    objectApiName: ACCOUNT_OBJECT.objectApiName,
    listViewApiName: 'AllAccounts'
  }) listInfo({ error, data }) {
    if (data) {
      var headers = [ "Name", "Phone" ];
      // create AOA
      var _aoa = [headers].concat(data.records.records.map(record => [
        record.fields.Name.value,  // Name field
        record.fields.Phone.value, // Phone field
      ]));

      // assign to state
      // highlight-next-line
      this.aoa = _aoa;
    } else if (error) console.log(error);
  };
}

Exporting Data

This is readily exported to a spreadsheet in a callback function. Starting from the array of arrays, the SheetJS aoa_to_sheet method3 generates a SheetJS sheet object4. A workbook object5 is created with book_new6 and the sheet is added with book_append_sheet7. Finally, the SheetJS writeFile method creates a XLSX file and initiates a download8.

  @api async download() {
    await loadScript(this, sheetjs); // load the library
    // get data from state
    // highlight-next-line
    var _aoa = this.aoa;

    // create workbook
    var wb = XLSX.utils.book_new();
    var ws = XLSX.utils.aoa_to_sheet(_aoa);
    XLSX.utils.book_append_sheet(wb, ws, "Data");

    // export
    XLSX.writeFile(wb, "SheetForceExport.xlsx");
  };

Complete Example

:::info pass

This demo was built on a "Developer Edition" account. At the time of writing, an account can be created for free.

:::

  1. Create a "Developer Edition" account. Take note of the unique Username

Configure Tools

  1. Install NodeJS LTS.

  2. Disable telemetry:

npx @salesforce/cli config set disable-telemetry=true --global
  1. Confirm the CLI tool works by checking version information:
npx @salesforce/cli --version

:::note pass

When the demo was last tested, the command printed

@salesforce/cli/2.10.2 darwin-x64 node-v16.20.2

:::

  1. Log into the org from the CLI tool:
npx @salesforce/cli org login web

This will open a web browser. Sign in and authorize the application.

Create Project

  1. Create the "SheetForce" sample project with the project generate command:
npx @salesforce/cli project generate -n SheetForce

Enter the project directory:

cd SheetForce
  1. Create a LWC component with the lightning generate component command:
npx @salesforce/cli lightning generate component --type lwc -n sheetComponent -d force-app/main/default/lwc

:::warning pass

At the time of testing, the CLI tool created components with app version 59. This version number is not valid for scratch orgs.

This is a bug in the Salesforce CLI

The workaround is to manually pin version 58 in the JSON and meta.xml files.

:::

  1. Replace force-app\main\default\lwc\sheetComponent\sheetComponent.html with the following template code:
<template>
  <!-- highlight-next-line -->
  <b>SheetForce demo</b>
</template>
  1. Replace force-app\main\default\lwc\sheetComponent\sheetComponent.js-meta.xml with the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
  <!-- highlight-start -->
  <apiVersion>58.0</apiVersion>
  <isExposed>true</isExposed>
  <masterLabel>SheetForce</masterLabel>
  <description>SheetJS Demo</description>
  <targets>
    <target>lightning__AppPage</target>
  </targets>
  <!-- highlight-end -->
</LightningComponentBundle>
  1. Edit sfdx-version.json and set the sourceApiVersion to 58.0:
  "name": "SheetForce",
  "namespace": "",
  "sfdcLoginUrl": "https://login.salesforce.com",
// highlight-next-line
  "sourceApiVersion": "58.0"
}

Deploy Sample Project

  1. Deploy the project from the CLI. You will need the Salesforce unique Username. For example, if the Username was SF@USER.NAME, the command is:
npx @salesforce/cli project deploy start -d force-app -o SF@USER.NAME
  1. Find the new component:

To find the new component in "Lightning Experience" view:

A) In the Salesforce site, click on the gear icon in the top-right corner of the page and select "Setup" (Setup for current app).

B) Type "Custom Code" in the left sidebar search box. Expand "Custom Code", expand "Lightning Components" and click "Lightning Components".

:::caution pass

With certain security settings, Salesforce will show an error:

We can't display this page because your browser blocks cross-domain cookies, but you can view this page in Salesforce Classic.

Click the link to open the page in Salesforce Classic.

:::

A) Click the "Setup" link in the top-right corner of the page.

B) Type "Lightning" in the left sidebar search box. Expand "Develop", expand "Lightning Components" and click "Lightning Components".

The page in Salesforce Classic will look like the screenshot below:

Custom Component

Initialize App Page

Create App Page

  1. Create an "App Page" in the "Lightning App Builder":

A) In the Salesforce site, click on the gear icon in the top-right corner of the page and select "Setup" (Setup for current app).

B) Type "App Build" in the left sidebar search box. Expand "User Interface" and click "Lightning App Builder".

:::caution pass

With certain security settings, Salesforce will show an error:

We can't display this page because your browser blocks cross-domain cookies, but you can view this page in Salesforce Classic.

Click the link to open the page in Salesforce Classic.

:::

C) Click the "New" button.

A) Click the "Setup" link in the top-right corner of the page.

B) Type "App Build" in the left search box and select "Lightning App Builder".

C) Click the "New" button.

App Wizard

D) Select "App Page" in the left list and click "Next"

E) Type "SheetJS Demo" in the Label textbox and click "Next"

F) Select "One Region" in the left list and click "Done"

App Builder

  1. Add the "SheetForce" component to the App Page.

In the left "Components" sidebar, under the "Custom" section, there should be a "SheetForce" entry.

Click and drag "SheetForce" into the "Add Component(s) Here" frame in the app builder main view to add it to the page.

Click "Save".

  1. Activate the page.

When the "Page Saved" modal is displayed, click "Activate".

The following options should be set:

  • Click "Change..." next to "Icon" and pick a memorable icon
  • Under "Lightning Experience" click "LightningBolt" then "Add page to app"

Click "Save" to activate the page.

  1. Open the demo page.

Click the left arrow in the top-left corner of the page to return to Setup.

If there is a "Switch to Lightning Experience" at the top, click the link.

Click the App Launcher (᎒᎒᎒) and search for "SheetJS". Under "Items", the new "SheetJS Demo" will be listed, Click "SheetJS Demo".

The app will display the "SheetForce demo" text from the component template:

Demo

Add SheetJS

16) Download https://cdn.sheetjs.com/xlsx-{current}/package/dist/xlsx.full.min.js

:::warning pass

DO NOT "COPY AND PASTE"! The file should be explicitly downloaded. Copying and pasting corrupts the source code and the component will fail in subtle ways.

The easiest approach is to right-click the link and select "Save Link As..."

:::

The following command can be run in PowerShell or bash:

{\ curl -o xlsx.full.min.js https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js}

  1. Move the file to the force-app/main/default/staticresources/ folder and rename the file to sheetjs.js.

If the file was downloaded from the previous command, mv can move and rename:

mv xlsx.full.min.js force-app/main/default/staticresources/sheetjs.js
  1. Create force-app/main/default/staticresources/sheetjs.resource-meta.xml (sheetjs.resource-meta.xml in the folder from step 2) with the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
  <cacheControl>Private</cacheControl>
  <contentType>application/javascript</contentType>
</StaticResource>
  1. Deploy the project again. Replace SF@USER.NAME with the unique Username:
npx @salesforce/cli project deploy start -d force-app -o SF@USER.NAME
  1. Look for the static resource:

A) In the Salesforce site, click on the gear icon in the top-right corner of the page and select "Setup" (Setup for current app).

B) Type "Static" in the left sidebar search box. Click "Static Resources"

:::caution pass

With certain security settings, Salesforce will show an error:

We can't display this page because your browser blocks cross-domain cookies, but you can view this page in Salesforce Classic.

Click the link to open the page in Salesforce Classic.

:::

A) Click the "Setup" link in the top-right corner of the page.

B) Type "Static" in the left sidebar search box. Click "Static Resources"

The page in Salesforce Classic will look like the screenshot below:

Static Resources

Test the Static Resource

  1. Replace force-app/main/default/lwc/sheetComponent/sheetComponent.js with the following script:
import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import sheetjs from '@salesforce/resourceUrl/sheetjs';

export default class SheetComponent extends LightningElement {
  version = "???"; // start with ???
  async connectedCallback() {
    await loadScript(this, sheetjs); // load the library
    // At this point, the library is accessible with the `XLSX` variable
    // highlight-next-line
    this.version = XLSX.version;
  }
}

This component exposes a version property based on the SheetJS version.

  1. Replace force-app/main/default/lwc/sheetComponent/sheetComponent.html with the following template:
<template>
  <!-- highlight-next-line -->
  <b>SheetForce {version}</b>
</template>

This template references the version property.

  1. Deploy the project again. Replace SF@USER.NAME with the unique Username:
npx @salesforce/cli project deploy start -d force-app -o SF@USER.NAME
  1. Reload the "SheetJS Demo" page. The page should display the SheetJS version:

Version number

:::info pass

It may take a few minutes for Salesforce to refresh. If the demo still shows the original "SheetForce demo" text after refreshing, close and reopen the demo app.

:::

Export Data from SF Lists

  1. Add a button to sheetComponent.html that will call a download callback:
<template>
  <!-- if the `aoa` property is set, show a button -->
  <template if:true={aoa}>
    <button onclick={download}><b>Click to Export!</b></button>
  </template>
  <!-- if the `aoa` property is not set, show a message -->
  <template if:false={aoa}><b>Please wait for data to load ...</b></template>
</template>
  1. Replace sheetComponent.js with the following:
import { LightningElement, wire, api } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import { getListUi } from 'lightning/uiListApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';

import sheetjs from '@salesforce/resourceUrl/sheetjs';

export default class SheetComponent extends LightningElement {
  aoa; // will hold data for export
  @wire(getListUi, {
    objectApiName: ACCOUNT_OBJECT.objectApiName,
    listViewApiName: 'AllAccounts'
  }) listInfo({ error, data }) {
    if (data) {
      var headers = [ "Name", "Phone" ];
      // create AOA and assign to `aoa` property
      this.aoa = [headers].concat(data.records.records.map(record => [
        record.fields.Name.value,  // Name field
        record.fields.Phone.value, // Phone field
      ]));
    } else if (error) console.log(error);
  };
  @api async download() {
    await loadScript(this, sheetjs); // load the library
    // create workbook
    var wb = XLSX.utils.book_new();
    var ws = XLSX.utils.aoa_to_sheet(this.aoa);
    XLSX.utils.book_append_sheet(wb, ws, "Data");
    // export
    XLSX.writeFile(wb, "SheetForceExport.xlsx");
  };
}
  1. Deploy the project again. Replace SF@USER.NAME with the unique Username:
npx @salesforce/cli project deploy start -d force-app -o SF@USER.NAME
  1. Reload the "SheetJS Demo" page. The page should include a button for export:

SF Export Button

  1. Click the "Click to Export!" button. The app will attempt to download a file.

The simple export includes all of the data:

Excel Export

:::tip pass

SheetJS Pro offers additional styling options like cell styling, automatic column width calculations, and frozen rows.

:::


  1. It is strongly recommended to review the detailed introduction in the Salesforce documentation ↩︎

  2. The XLSX variable is the main global for the SheetJS library. It exposes methods as described in "API Reference" ↩︎

  3. See aoa_to_sheet in "Utilities" ↩︎

  4. See "Sheet Objects" ↩︎

  5. See "Workbook Object" ↩︎

  6. See book_new in "Utilities" ↩︎

  7. See book_append_sheet in "Utilities" ↩︎

  8. See writeFile in "Writing Files" ↩︎