Arsalan Khattak
13 February 2024

Using Bryntum Scheduler Pro as a Salesforce Lightning Web Component

Bryntum Scheduler Pro and Salesforce Lightning Web Component
Salesforce Lightning Web Components (LWC) is a modern framework for building web applications on the Salesforce platform using standard web technologies like […]

Salesforce Lightning Web Components (LWC) is a modern framework for building web applications on the Salesforce platform using standard web technologies like JavaScript, HTML, and CSS.

In this tutorial, we’ll create a location-based work task scheduler using Bryntum Scheduler Pro in Salesforce. We’ll do the following:

Prerequisites

If you don’t have a Salesforce account or want a Salesforce Platform playground, sign up for the Salesforce Developer Edition, a free, full-featured copy of the Salesforce Platform.

You’ll need to install the Salesforce CLI to create a Lightning web component and deploy it to your Salesforce organization. Install the CLI from here. Visual Studio Code (VS Code) is the recommended IDE for Salesforce development. Install the recommended VS Code extension: Salesforce Extension Pack. Please be aware that the extension pack relies on the presence of Java on your system. For guidance on configuring Java JDK with VS Code, you can find more information here.

We’ll create a Salesforce scratch organization to develop and test the Bryntum Scheduler Pro LWC. A scratch org is a disposable deployment of Salesforce code and metadata. Note that you should not use personal data in a scratch org. The Salesforce CLI and scratch orgs are part of the Salesforce Developer Experience (DX) product suite. To create a scratch org, you’ll need to log in to your Salesforce developer environment and enable Dev Hub features in your org.

Create a Salesforce DX project

Follow these steps to create a Salesforce DX project:

  1. Create a base directory for your project.
  2. In VS Code, open the command palette by pressing Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS).
  3. Type SFDX and select SFDX: Create Project.
  4. Select the default Standard Project Template.
  5. Give your project a name, for example, BryntumSchedulerPro.
  6. Select the base directory you created to store your project in.

Your Salesforce DX project will include various folders and files for developing with Salesforce, such as .sfdxconfig, and force-app.

Authorize a Dev Hub

Open the VS Code command palette, then type and select SFDX: Authorize a Dev Hub. Use the default alias. The Salesforce login page will open in a new browser window. Log in using your Salesforce Developer Edition account. Click the “Allow” button when prompted to allow the Salesforce CLI access to your org. The Salesforce CLI remembers your credentials once you’ve authenticated your Dev Hub in the browser.

Create a scratch org

In the VS Code command palette, type and select the following:

SFDX: Create a Default Scratch Org

You can choose the defaults when prompted.

Now, open the VS Code command palette again, then type and select the following:

SFDX: Open Default Org

In the Salesforce Platform UI, do the following:

  1. Click on the “Setup gear” icon on the top right and click “Setup” in the popup menu.
  2. In the “Quick Find” input on the top left, search for “My Domain” and select it.
  3. Under My Domain Details, copy the “Current My Domain URL”.

In your Salesforce code in VS Code, open the sfdx-project.json file and set sfdcLoginUrl to your current My Domain URL.

Create the static resources for the Bryntum Scheduler Pro Lightning web component

The Bryntum Scheduler Pro LWC will need the library code for the Bryntum Scheduler Pro. We’ll upload the library code to Salesforce as static resources accessible to your Bryntum Scheduler Pro LWC.

The resources required for the Bryntum Scheduler Pro LWC are:

To get these static resources, download the free trial version distribution folder of the Bryntum Scheduler Pro here. If you have already bought the licensed version, you can log in here to download the Bryntum Scheduler Pro distribution folder.

In the /force-app/main/default/staticresources folder in your Salesforce DX project, create a static resource metadata file named bryntum_schedulerpro.resource-meta.xml, and add the following lines of code to it:

<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
    <cacheControl>Private</cacheControl>
    <contentType>application/zip</contentType>
</StaticResource>

In the /force-app/main/default/staticresources folder, create a folder called bryntum_schedulerpro.

Now copy the following files and folders from the /build folder in the Bryntum Scheduler Pro distribution folder:

Add them to your /force-app/main/default/staticresources/bryntum_schedulerpro folder.

Add the Bryntum Scheduler Pro static resources to your Salesforce org by right-clicking on the staticresources folder in the VS Code Explorer pane and selecting:

SFDX: Deploy Source to Org

In your Salesforce scratch org, click the setup gear icon on the top right and click “Setup” in the popup menu. Go to the “Quick Find” search input and search for “Static Resources”. You should see a folder named “bryntum_schedulerpro” in the table.

Create a custom object in Salesforce to store resources data

We’ll create two custom Salesforce objects to store our tasks and resources data. Let’s create the custom object for resources first.

Import resources data from a CSV file

We’ll create most of the fields for the custom objects and add some initial data by importing data from spreadsheets, as this is faster than creating each field manually in the Salesforce UI.

In your Salesforce scratch org:

  1. Open “Setup” by clicking the gear icon on the top right of the page.
  2. Navigate to the “Object Manager” tab.
  3. At the top of this tab, right-click the “Create” button.
  4. In the dropdown menu, click “Custom Object from Spreadsheet”.
  5. In the new tab, click the “Login with Sandbox” button.
  6. In the login form, click “Use Custom Domain”. Use the domain URL you added to the sfdcLoginUrl property in your sfdx-project.json file.

To get the username for your scratch org, run the following command in your VS Code terminal:

sf org:list --all

To generate a password for the username, run the following command:

sf org generate password --target-org <alias>

Replace <alias> with your scratch org alias.

Once you’ve been authenticated, copy and paste the following data into a bryntum-schedulerpro-resources-data.csv file on your computer:

Name, Role, Event Color
James Smith, Service technician, #3183fe
Lisa van Wyk, IT technician, #0076f8z
Jane Hansen, Field technician, #9e25c3
Mark May, Plumber, #2055a5
Peter Freeman, Electrician, #1fba5e
Kate Davies, IT technician, #fab007

The headings in this CSV text represent some of the fields for a Bryntum Scheduler Pro resource.

In the “SALESFORCE FIELD TYPE” column, change the field types as follows:

Click the “Next” button at the bottom right.

Set the labels and API name for the custom resources object

For the Object Properties, set “Label” to bryntum-schedulerpro-resources-data, “Plural Label” to bryntum-schedulerpro-resources-data, and “API Name” to bryntum_schedulerpro_resources_data. Click the “Finish” button at the bottom right.

Once the object is created, go to the “Object Manager” tab in “Setup” and search for your new custom object using the “Quick Find” input at the top right of the “Object Manager” tab. You’ll notice that the API Name has a __c added to the end. This indicates that it’s a custom object.

Create a custom object in Salesforce to store tasks data

Now we’ll create a custom object for the tasks.

Import tasks data from a CSV file

Create a new custom object from a spreadsheet in the same way you created the resources custom object. The CSV file you use should be named bryntum-schedulerpro-tasks-data.csv and contain the following CSV values:

Name, Read Only, Time Zone, Draggable, Resizable, Children, All Day, Duration, Duration Unit, Start Date, Exception Dates, Recurrence Rule, Cls, Event Color, Event Style, Icon Cls, Style, Preamble, Postamble, Address, Percent Done, Effort, Effort Unit
Fix server, 0, , 0, , , 0, 2.0, h, 2024-12-15T08:30:00Z, , , , , , , , 10min, 10min, "{}", 2, 2 ,hour
Unblock drain, 0, , 0, , , 0, 2.0, h, 2024-12-15T14:00:00Z, , , , , , , , 1h, 30min, "{}", 0, 0 , hour
Update software, 0, , 0, , , 0, 3.0, h, 2024-12-15T11:00:00Z, , , , , , , , 20min, 20min, "{}", 0, 0 , hour
Inspect cables, 0, , 0, , , 0, 3.0, h, 2024-12-15T15:00:00Z, , , , , , , , 1h, 1h, "{}", 0, 0 , hour
Maintenance, 0, , 0, , , 0, 3.0, h, 2024-12-15T12:00:00Z, , , , , , , , 20min, 30min, "{}", 0, 0 , hour
Repair cables, 0, , 0, , , 0, 2.5, h, 2024-12-15T11:30:00Z, , , , , , , , 30min, 30min, "{}", 0, 0 , hour

The headings in this CSV text represent fields for a Bryntum Scheduler Pro event (or “task”). In the “SALESFORCE FIELD TYPE” column, change the field types as follows:

Set the labels and API name for the custom tasks object

For the Object Properties, set “Label” to bryntum-schedulerpro-tasks-data, “Plural Label” to bryntum-schedulerpro-tasks-data, and “API Name” to bryntum_schedulerpro_tasks_data.

Add the resource ID field as a lookup field to connect tasks to resources.

We’ll now add the resource ID field as a lookup field. The special lookup field type allows you to connect two Salesforce objects. The resource ID field connects a task to a resource. Follow these steps to create the lookup field:

  1. In the Object Manager tab in Setup, search for the bryntum-schedulerpro-tasks-data object using the “Quick Find” input at the top right of the tab. You’ll notice that the API Name has a __c added to the end. This indicates that it’s a custom object.
  2. In the “Fields and Relationships” tab on the left, click the “New” button.
  3. Select the data type “Lookup Relationship” and click “Next”.
  4. In the “Related To” dropdown list, select “bryntum_schedulerpro_resources_data”, and click “Next”.
  5. Set the “Field Label” to Resource Id and the “Field Name” to Resource_Id.
  6. Click “Next” three times and then click “Save”.

Link example tasks to resources using the Salesforce UI

You can link the example tasks to resources using the Salesforce UI by doing the following:

  1. Click on the “App Launcher” icon at the top left of the page and search for “bryntum-schedulerpro-tasks-data”. A table containing the tasks data you imported from the CSV file will open.
  2. Click on the “Recently Viewed” dropdown list at the top left and select “All Records”.
  3. Click the dropdown arrow button at the end of one of the task rows and select “Edit” in the popup menu.
  4. In the “Resource Id” field, select the resource the task will link to. In this case, the resource is a person. You’ll need to input the resource ID to select a resource. You can find the resource IDs in the “bryntum-schedulerpro-resources-data” custom object.

Add the custom resources and tasks objects to your Salesforce DX project

In your VS Code terminal, run the following command to add your custom tasks object to your Salesforce DX project:

sf project retrieve start -m CustomObject:bryntum_schedulerpro_tasks_data__c

Now run the following command to add your custom resources object to your Salesforce DX project:

sf project retrieve start -m CustomObject:bryntum_schedulerpro_resources_data__c

Create a Bryntum Scheduler Pro Lightning web component

Now let’s create a Bryntum Scheduler Pro LWC. We’ll define some custom classes for the Bryntum Scheduler Pro, including one to add a custom address search field to the task editor. We’ll also add a custom event renderer to the task bar.

Create a basic Lightning web component skeleton

Open the VS Code command palette, then type and select SFDX: Create Lightning Web Component. Enter schedulerpro as the name of the new component. Press Enter to accept the default directory: /force-app/main/default/lwc. This creates a schedulerpro folder in the /force-app/main/default/lwc folder. The new folder contains an HTML file and a JavaScript file for the Lightning web component, and an XML configuration file named schedulerpro.js-meta.xml that defines the component’s metadata values.

Replace the code in the schedulerpro.js-meta.xml with the following:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
  <apiVersion>58.0</apiVersion>
  <isExposed>true</isExposed>
  <masterLabel>Bryntum Scheduler Pro</masterLabel>
  <description>Bryntum Scheduler Pro sample</description>
  <targets>
    <target>lightning__AppPage</target>
    <target>lightning__Tab</target>
    <target>lightningCommunity__Page</target>
  </targets>
</LightningComponentBundle>

We expose the LWC so that it can be used in the Lightning App Builder in your Salesforce scratch org. We also set the targets specifying where the LWC can be added. The LWC can be added to a Lightning App Page.

Replace the code in the schedulerpro.html file with the following lines of code:

<template>
  <div class="container" lwc:dom="manual"></div>
</template>

This HTML file uses an LWC HTML template to render components efficiently using a virtual DOM. In most cases, it’s best to let the LWC manipulate the DOM instead of using JavaScript. For the Bryntum Scheduler Pro LWC, we set lwc:dom to "manual" as the Bryntum Scheduler Pro JavaScript code will need to manipulate the DOM by adding the Scheduler Pro component to the <div> with a class of "container".

Add CSS styles to the Lightning web component

Now create a schedulerpro.css file and add the following lines of code to it:

.container {
  height: 45em;
}
.b-float-root {
  z-index: 100;
}
.address-results .b-list-item i {
  font-size: 1.5em;
  margin: 0 0.7em 0 0.2em;
}
.address-container {
  display: flex;
  flex-direction: column;
}
.address-name {
  flex: 1;
  margin-bottom: 0.4em;
  font-size: 1.1em;
}
.lat-long {
  flex: 1;
  color: #bbb;
  font-size: 0.9em;
}
.b-sch-event {
  border-radius: 3px;
  box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.4);
  border: 1px solid rgba(0, 0, 0, 0.2);
}
.b-theme-material .widget-title {
  color: #555;
}
.b-sch-event-content {
  display: flex;
  height: 100%;
  flex-direction: column;
  justify-content: space-evenly;
}
.event-name {
  font-size: 1.3em;
}
.location {
  display: block;
  margin-top: 0.3em;
  font-weight: normal;
}
.b-fa-map-marker-alt {
  margin-inline: 1px 0.4em;
}

The CSS styles we add here will size the Scheduler Pro in the <div> with a class of "container", style the task bar, and style the custom address input that we’ll add to the task editor.

Create a Bryntum Scheduler Pro Lightning web component

In the schedulerpro.js file, the LightningElement class is the base class for Lightning Web Components. We’ll extend it to create the Schedulerpro class that will create the Bryntum Scheduler Pro. Replace the code in the schedulerpro.js file with the following lines of code:

/* globals bryntum : true */
import { LightningElement } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadScript, loadStyle } from "lightning/platformResourceLoader";
import SCHEDULERPRO from "@salesforce/resourceUrl/bryntum_schedulerpro";
import ScheduleMixin from "./lib/Schedule";
import TaskMixin from "./lib/Task";
export default class Schedulerpro extends LightningElement {
  schedulerproData = null;
  renderedCallback() {
    if (this.bryntumInitialized) {
      return;
    }
    this.bryntumInitialized = true;
    Promise.all([
      loadScript(this, SCHEDULERPRO + "/schedulerpro.lwc.module.js"),
      loadStyle(this, SCHEDULERPRO + "/schedulerpro.stockholm.css"),
    ])
      .then(() => {
        this.loadAndPrepareData();
      })
      .catch((error) => {
        this.dispatchEvent(
          new ShowToastEvent({
            title: "Error loading Bryntum Scheduler Pro",
            message: error,
            variant: "error",
          })
        );
      });
  }
  async loadAndPrepareData() {
    let data;
    try {
      data = await getData();
    } catch (error) {
      console.error("Error getting events and resources data: ", { error });
      return; // If there's an error, we don't want to proceed
    }
    this.schedulerproData = mapData(data);
    this.createScheduler();
  }
  createScheduler() {
    const container = this.template.querySelector(".container");
    const { SchedulerPro, EventModel, ProjectModel } = bryntum.schedulerpro;
    const Schedule = ScheduleMixin(SchedulerPro);
    const Task = TaskMixin(EventModel);
    const project = new ProjectModel({
      eventModelClass: Task,
      eventsData: this.schedulerproData.tasks,
      resourcesData: this.schedulerproData.resources,
    });
    const schedule = window.schedule = new Schedule({
      ref: "schedule",
      appendTo: container,
      startDate: new Date(2024, 11, 15, 8),
      endDate: new Date(2024, 11, 15, 20),
      project,
    });
  }
}

The schedulerproData property will store the data we get from our custom Salesforce objects. The renderedCallback() method is called after every render of the Lightning web component. Once the Bryntum Scheduler Pro JavaScript and CSS static resources are loaded using the Lightning Web Components Platform Resource Loader  loadStyle() and loadScript() methods, the loadAndPrepareData() method is called. This method will get the Bryntum Scheduler Pro data and then create the Bryntum Scheduler Pro instance. The getData and mapData methods will be defined later.

In the createScheduler() method, we define the project model that will store the tasks and resources data and link them together. We use a custom event model for tasks so that we can add an address field to the tasks. We also create a Bryntum Scheduler Pro instance using a custom Scheduler Pro class.

Create a custom Task class

Now let’s define the custom classes. Create a lib folder in the schedulerpro LWC folder and create a file called Task.js. Add the following lines of code to it:

export default (EventModelClass) =>;
  class Task extends EventModelClass {
    // Custom Task model, based on EventModel with additional fields and changed defaults
    static get fields() {
      return [
        { name: "address", defaultValue: {} },
        { name: "duration", defaultValue: 1 },
        { name: "durationUnit", defaultValue: "hour" },
      ];
    }
    get shortAddress() {
      return (this.address?.display_name || "").split(",")[0];
    }
  };

The custom Task model extends the Scheduler Pro EventModel. We wrap the class in a function so that we can pass in the EventModel as an argument from where the class is instantiated. The custom Task model sets some field value defaults and adds a custom address field to show location data. The address field will be an object containing a display name, latitude, and longitude for a location.

Create a custom Address class

Now create a file called Address.js in the lib folder and add the following lines of code to it:

export default (EventModelClass) =>
  class Address extends EventModelClass {
    static get fields() {
      return ["display_name", "lat", "lon"];
    }
  };

We create another custom EventModel to define the data fields of the custom Address field that we’ll add to the task editor.

Create custom Schedule and AddressSearchField classes

Let’s define the custom Schedule class. Create a file called Schedule.js in the lib folder and add the following lines of code to it:

import AddressMixin from "./Address.js";
export default (SchedulerProClass) => {
  const { DateHelper, StringHelper, Combo, EventModel } =
    window.bryntum.schedulerpro;
  class AddressSearchField extends Combo {
    // Factoryable type name
    static get type() {
      return "addresssearchfield";
    }
    static get $name() {
      return "AddressSearchField";
    }
    static get configurable() {
      const Address = AddressMixin(EventModel);
      return {
        clearable: true,
        displayField: "display_name",
        // Setting the value field to null indicates we want the Combo to get/set address *records* as opposed to the
        // id of an address record.
        valueField: null,
        filterOnEnter: true,
        minChars: 4,
        store: {
          modelClass: Address,
          readUrl: "https://nominatim.openstreetmap.org/?format=json&q=",
          restfulFilter: true,
          fetchOptions: {
            // Please see MDN for fetch options: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
            credentials: "omit",
          },
        },
        // Addresses can be long
        pickerWidth: 450,
        validateFilter: false,
        listCls: "address-results",
        // Custom list item template to show a map icon with lat + lon
        listItemTpl: (address) => {
          return `
                    
${address.display_name} ${address.lat}°, ${address.lon}°
`; }, }; } } AddressSearchField.initClass(); return class Schedule extends SchedulerProClass { // Customized scheduler with address search field static get $name() { return "Schedule"; } static get configurable() { return { features: { stripe: true, eventBuffer: true, taskEdit: { saveAndCloseOnEnter: false, editorConfig: { autoUpdateRecord: false, }, items: { generalTab: { items: { resourcesField: { required: true, }, // For this demo we add an extra remote address search field addressField: { type: "addresssearchfield", label: "Address", name: "address", weight: 100, }, preambleField: { label: "Travel to", }, postambleField: { label: "Travel from", }, }, }, }, }, }, rowHeight: 80, barMargin: 4, eventColor: null, eventStyle: null, columns: [ { type: "resourceInfo", text: "Name", width: 200, showEventCount: true, showRole: true, showImage: false, }, ], // Custom view preset with header configuration viewPreset: "hourAndDay", tbar: [ { type: "widget", cls: "widget-title", html: "Scheduler View", flex: 1, }, { type: "datefield", ref: "dateField", width: 190, editable: false, step: 1, onChange: "up.onDateFieldChange", }, { type: "textfield", ref: "filterByName", placeholder: "Filter tasks", clearable: true, keyStrokeChangeDelay: 100, triggers: { filter: { align: "start", cls: "b-fa b-fa-filter", }, }, onChange: "up.onFilterChange", }, ], }; } construct() { super.construct(...arguments); this.widgetMap.dateField.value = this.startDate; } onFilterChange({ value }) { value = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // Replace all previous filters and set a new filter this.taskStore.filter({ filters: (event) => new RegExp(value, "i").test(event.name), replace: true, }); } onDateFieldChange({ value, userAction }) { userAction && this.setTimeSpan( DateHelper.add(value, 8, "hour"), DateHelper.add(value, 20, "hour") ); } onPrevious() { this.shiftPrevious(); } onNext() { this.shiftNext(); } // Custom event renderer showing the task name + location icon with a shortened address text eventRenderer({ eventRecord }) { return [ { tag: "span", className: "event-name", html: StringHelper.encodeHtml(eventRecord.name), }, { tag: "span", className: "location", children: [ eventRecord.shortAddress ? { tag: "i", className: "b-fa b-fa-map-marker-alt", } : null, eventRecord.shortAddress || "⠀", ], }, ]; } }; };

The wrapper function for this file has two classes: AddressSearchField and Schedule, which are returned from the wrapper function.

The custom AddressSearchField is a customized Combo (dropdown) widget that uses the Nominatim API to query for location data by address. The minChars property is the minimum string length that triggers filtering when the enter button is pressed. We add this custom field to our Scheduler Pro task editor by adding it to the taskEdit feature in the defaultConfig() static getter method of the custom Schedule class.

The Schedule class has a custom eventRenderer that displays the task name and shortened address name on the task bar.

Add the nominatim API URL to the CSP Trusted Sites in Salesforce

In the Salesforce Platform UI, click on the setup gear icon on the top right and click “Setup” in the popup menu. In the “Quick Find” input on the top left, search for “Trusted URLs”. Add “https://nominatim.openstreetmap.org” as a trusted site. Under the “CSP Directives” header, check the “connect-src (scripts)” checkbox and then click “Save”.

Now let’s load our custom Salesforce object data into our Bryntum Scheduler Pro LWC by defining the getData function.

Query events and resources data from the custom objects

When working with Salesforce data, you can use the Lightning Data Service to load, create, edit, or delete a Salesforce record. For more complex data operations, such as making a Salesforce Object Query Language (SOQL) query to select certain records, you can use Apex to write server-side controller classes. Let’s create an Apex class to get our tasks and resources data from our custom Salesforce objects.

public with sharing class SchedulerproDataLoadController {
  @AuraEnabled(cacheable=true)
    
  public static Map<String, Object> getData() { 
    // fetching the Records via SOQL
    List<bryntum_schedulerpro_tasks_data__c> Tasks = new List<bryntum_schedulerpro_tasks_data__c>();
    Tasks = [
      SELECT Address__c,
      All_day__c,
      Children__c,
      Cls__c,
      Draggable__c,
      Duration__c,
      Duration_Unit__c,
      Event_Color__c,
      Event_Style__c,
      Exception_Dates__c,
      Icon_Cls__c,
      Name__c,
      Postamble__c,
      Preamble__c,
      Percent_Done__c,
      Effort__c,
      Effort_Unit__c,
      Read_Only__c,
      Recurrence_Rule__c,
      Resizable__c,
      Resource_Id__c,
      Start_Date__c,
      Style__c,
      Time_Zone__c
      FROM bryntum_schedulerpro_tasks_data__c WITH SECURITY_ENFORCED
    ];
    
    List<bryntum_schedulerpro_resources_data__c> Resources = new List<bryntum_schedulerpro_resources_data__c>();
    Resources = [
      SELECT Event_Color__c,
      Name__c,
      Role__c
      FROM bryntum_schedulerpro_resources_data__c WITH SECURITY_ENFORCED
    ];
    Map<String, Object> result = new Map<String, Object>{
      'tasks' => Tasks, 'resources' => Resources };
    return result;
  }
}

The AuraEnabled annotation enables our Lightning web component to use Apex methods and properties. We set cacheable=true so that the results are cached to improve runtime performance.

In Salesforce Apex, collections are used to store data. There are three types: lists, sets, and maps. The getTasks Apex method returns a Map type, where the keys are strings and the values are objects. The getData Apex method creates two lists. The type of elements that will be stored in the Tasks list are records with the object type of the bryntum_schedulerpro_tasks_data__c custom object. The type of elements stored in the Resources list are records with the object type of the bryntum_schedulerpro_resources_data__c custom object. The __c indicates that it’s a custom object, this is a Salesforce naming convention.

Each list performs a SOQL read operation to get the tasks or resources data. The result of the query is assigned to the list to store the values. The getData method returns a map of the tasks and resources data.

Add the Apex class to your Salesforce org

Add the Apex class to your Salesforce org by right-clicking the classes folder in the VS Code Explorer pane and selecting SFDX: Deploy Source to Org.

Load data from the custom objects into the Bryntum Scheduler Pro

To get the queried data into your Bryntum Scheduler Pro LWC, import the Apex method in the schedulerpro.js file:

import getData from "@salesforce/apex/SchedulerproDataLoadController.getData";

This method is called in the loadAndPrepareData method.

Before we use the data in the Scheduler Pro data stores, we need to change the property names from sentence case with words separated by an underscore to camel case and remove the __c suffix. We use the mapData function to do this. It’s also called in the loadAndPrepareData method.

Define the mapData function above the SchedulerPro class definition as follows:

function mapData(fromSF) {
  const tasks = fromSF.tasks.map((item) => ({
    id: item.Id,
    address: item.Address__c ? JSON.parse(item.Address__c) : null,
    allDay: item.All_Day__c,
    children: item.Children__c,
    cls: item.Cls__c,
    draggable: item.Draggable__c,
    duration: item.Duration__c,
    durationUnit: item.Duration_Unit__c,
    color: item.Event_Color__c,
    style: item.Event_Style__c,
    exceptionDates: item.Exception_Dates__c
      ? JSON.parse(item.Exception_Dates__c)
      : null,
    cls: item.Icon_Cls__c,
    name: item.Name__c,
    postamble: item.Postamble__c,
    preamble: item.Preamble__c,
    percentDone: item.Percent_Done__c,
    effort: item.Effort__c,
    effortUnit: item.Effort_Unit__c,
    readOnly: item.Read_Only__c,
    recurrenceRule: item.Recurrence_Rule__c,
    resizable: item.Resizable__c,
    resourceId: item.Resource_Id__c,
    startDate: item.Start_Date__c,
    style: item.Style__c,
    timeZone: item.Time_Zone__c,
  }));
  const resources = fromSF.resources.map((item) => ({
    id: item.Id,
    eventColor: item.Event_Color__c,
    name: item.Name__c,
    role: item.Role__c,
  }));
  return { tasks, resources };
}

The Address field is an object, and the Exception Dates field can be a string or an array. These fields are stored as strings in the Salesforce tasks object for simplicity, so they need to be parsed before they are added to the Bryntum Scheduler Pro event store. The data objects are stored in the schedulerproData variable and added to the Scheduler Pro in the project model configuration.

The Bryntum Scheduler will now have access to the initial tasks and resources data. Next, we’ll add our Bryntum Scheduler Pro LWC to our Salesforce scratch org.

Add the Bryntum Scheduler Pro component to a Salesforce Lightning App

In the VS Code explorer pane, right-click on the schedulerpro LWC folder and select SFDX: Deploy Source to Org. Open the VS Code command palette, then type and select SFDX: Open Default Org. This opens your Salesforce scratch org in a browser window. In the “Quick Find” input at the top left of the page in the navigation column, search for “Lightning App Builder” and click on it. Follow these steps to create a Lightning Page with your Bryntum Scheduler Pro LWC:

  1. Click the “New” button to create a Lightning page.
  2. Click the “Next” button on the “App Page” tab.
  3. Give the page the label “Bryntum Scheduler Pro”.
  4. Select “One Region” for the layout type.
  5. Find the “Bryntum Scheduler Pro” custom component at the bottom of the components column on the left of the page, and drag it into the block with the label “Add Component(s) Here” in the center of the page.
  6. Click the “Save” button and then the “Activate” button.
  7. Click the “Save” button in the Activation dialog and then click “Finish”.

Now click the back button on the top left corner of the page. Open the App Launcher by clicking the nine-dot button at the top left of the page and search for “Bryntum Scheduler Pro”. You should see your Bryntum Scheduler Pro LWC in the Lightning Page:

Double-click on one of the events to open the task editor. To add an address, add at least four characters to the “ADDRESS” field input and press the Enter key.

Now let’s save changes made to the Bryntum Scheduler Pro LWC to our tasks and resources custom Salesforce objects.

Create or update tasks in the Scheduler Pro task editor

In the schedulerpro.js file, import the createRecordupdateRecord, and deleteRecord methods from the Salesforce lightning/uiRecordApi module:

import {
  createRecord,
  deleteRecord,
  updateRecord,
} from "lightning/uiRecordApi";

The Salesforce lightning/uiRecordApi module allows us to make requests to Salesforce JavaScript APIs to create, delete, or update records in our custom objects.

Now add the following event listeners to the schedule config:

      listeners: {
        beforeTaskEditShow() {
          disableChange = true;
        },
        afterTaskSave({ taskRecord }) {
          disableChange = false;
          // create / update event
          updateOrCreateTask(taskRecord);
        },
      },

Add the disableChange variable at the top of the file:

let disableChange = false;

These event listeners are used to disable creating or updating the Salesforce custom tasks object while the task editor is open. This prevents unnecessary API calls.

Let’s define the updateOrCreateTask function at the top of the createScheduler() method:

    updateOrCreateTask(taskRecord) {
      // create
      if (taskRecord.isPhantom) {
        try {
          const insertEvent = {
            apiName: "bryntum_schedulerpro_tasks_data__c",
            fields: {
              Address__c: taskRecord.address.display_name
                ? JSON.stringify({
                    display_name: taskRecord.address.display_name,
                    lat: taskRecord.address.lat,
                    lon: taskRecord.address.lon,
                  })
                : null,
              All_Day__c: Number(taskRecord.allDay),
              Children__c: taskRecord.children,
              Cls__c: taskRecord.cls,
              Draggable__c: Number(taskRecord.draggable),
              Duration__c: taskRecord.duration,
              Duration_Unit__c: taskRecord.durationUnit,
              Event_Color__c: taskRecord.color,
              Event_Style__c: taskRecord.style,
              Exception_Dates__c: JSON.stringify(taskRecord.exceptionDates),
              Icon_Cls__c: taskRecord.cls,
              Name__c: taskRecord.name,
              Postamble__c: taskRecord.postamble,
              Preamble__c: taskRecord.preamble,
              Percent_Done__c: taskRecord.percentDone,
              Effort__c: taskRecord.effort,
              Effort_Unit__c: taskRecord.effortUnit,
              Read_Only__c: Number(taskRecord.readOnly),
              Recurrence_Rule__c: taskRecord.recurrenceRule,
              Resizable__c: `${taskRecord.resizable}`,
              Resource_Id__c: taskRecord.resourceId,
              Start_Date__c: formatDate(taskRecord.startDate),
              Style__c: taskRecord.style,
              Time_Zone__c: taskRecord.timeZone,
            },
          };
          return createRecord(insertEvent).then((resEvent) => {
            schedule.taskStore.applyChangeset({
              updated: [
                // This will set the proper id for the added task
                { $PhantomId: taskRecord.id, id: resEvent.id },
              ],
            });
          });
        } catch (error) {
          console.error(error);
        }
      } else {
        // update
        try {
          const updateEvent = {
            fields: {
              Id: taskRecord.id,
              Address__c: taskRecord.address
                ? JSON.stringify({
                    display_name: taskRecord.address.display_name,
                    lat: taskRecord.address.lat,
                    lon: taskRecord.address.lon,
                  })
                : null,
              All_Day__c: Number(taskRecord.allDay),
              Children__c: taskRecord.children,
              Cls__c: taskRecord.cls,
              Draggable__c: Number(taskRecord.draggable),
              Duration__c: taskRecord.duration,
              Duration_Unit__c: taskRecord.durationUnit,
              Event_Color__c: taskRecord.color,
              Event_Style__c: taskRecord.style,
              Exception_Dates__c: JSON.stringify(taskRecord.exceptionDates),
              Icon_Cls__c: taskRecord.cls,
              Name__c: taskRecord.name,
              Postamble__c: taskRecord.postamble,
              Preamble__c: taskRecord.preamble,
              Percent_Done__c: taskRecord.percentDone,
              Effort__c: taskRecord.effort,
              Effort_Unit__c: taskRecord.effortUnit,
              Read_Only__c: Number(taskRecord.readOnly),
              Recurrence_Rule__c: taskRecord.recurrenceRule,
              Resizable__c: `${taskRecord.resizable}`,
              Resource_Id__c: taskRecord.resourceId,
              Start_Date__c: formatDate(taskRecord.startDate),
              Style__c: taskRecord.style,
              Time_Zone__c: taskRecord.timeZone,
            },
          };
          return updateRecord(updateEvent).then((resEvent) => {
            console.log("resEvent for update: ", resEvent);
          });
        } catch (error) {
          console.error(error);
        }
      }
    }

This function creates a new task if the event has a phantom ID, which is an autogenerated unique value created by the Scheduler Pro on the client to identify a record. The “real” ID will be added by the Salesforce backend. The updateOrCreateTask function updates a task if it has an ID assigned to it by Salesforce on the backend.

Add the following formatDate helper function to the top of the file:

function formatDate(dateString) {
  let date = new Date(dateString);
  return date.toISOString();
}

We also need to listen for data changes that happen outside of the task editor to update the Bryntum Scheduler Pro data in Salesforce.

Listen for store data changes in the Bryntum Scheduler Pro LWC

In the ProjectModel  configuration, add the following listeners property below the resourcesData property:

      listeners: {
        change({ store, action, records }) {
          const storeId = store.id;
          if (!disableChange && storeId === "events") {
            if (action === "add") {
              console.log("add event");
            }
            if (action === "remove") {
              console.log("remove event");
              if (records[0].isPhantom) return;
              records.forEach((record) => {
                deleteRecord(record.id).then((res) =>
                  console.log("deleteRecord: ", { res })
                );
              });
            }
            if (action === "update") {
              for (let i = 0; i < records.length; i++) {
                const taskRecord = records[i];
                if (taskRecord.isPhantom) continue;
                try {
                  const updateEvent = {
                    fields: {
                      Id: taskRecord.id,
                      Address__c: taskRecord.address
                        ? JSON.stringify({
                            display_name: taskRecord.address.display_name,
                            lat: taskRecord.address.lat,
                            lon: taskRecord.address.lon,
                          })
                        : null,
                      All_Day__c: Number(taskRecord.allDay),
                      Children__c: taskRecord.children,
                      Cls__c: taskRecord.cls,
                      Draggable__c: Number(taskRecord.draggable),
                      Duration__c: taskRecord.duration,
                      Duration_Unit__c: taskRecord.durationUnit,
                      Event_Color__c: taskRecord.color,
                      Event_Style__c: taskRecord.style,
                      Exception_Dates__c: JSON.stringify(
                        taskRecord.exceptionDates
                      ),
                      Icon_Cls__c: taskRecord.cls,
                      Name__c: taskRecord.name,
                      Postamble__c: taskRecord.postamble,
                      Preamble__c: taskRecord.preamble,
				              Percent_Done__c: taskRecord.percentDone,
				              Effort__c: taskRecord.effort,
				              Effort_Unit__c: taskRecord.effortUnit,
                      Read_Only__c: Number(taskRecord.readOnly),
                      Recurrence_Rule__c: taskRecord.recurrenceRule,
                      Resizable__c: `${taskRecord.resizable}`,
                      Resource_Id__c: taskRecord.resourceId,
                      Start_Date__c: formatDate(taskRecord.startDate),
                      Style__c: taskRecord.style,
                      Time_Zone__c: taskRecord.timeZone,
                    },
                  };
                  return updateRecord(updateEvent).then((resEvent) => {
                    console.log("resEvent for update: ", resEvent);
                  });
                } catch (error) {
                  console.error(error);
                }
              }
            }
          }
        },
      },

The change method is called when data in a project store changes. We get information about the store change from the method parameters. The store object allows us to determine which store changed, tasks or resources. The action is the name of the action that triggered the change, such as “add”, “remove”, or “update”. There are other events, like “removeAll”, “updateMultiple”, “filter”, “dataset”, and “replace”. We have excluded them here for simplicity. The records are an array of all of the changed records, tasks or resources. The change method has nested if statements that will allow us to perform the correct CRUD operation for a custom object store update using the Salesforce lightning/uiRecordApi module methods.

You could further customize your Bryntum Scheduler Pro LWC using the Bryntum assignmentStore and dependencyStore, which we didn’t include in this tutorial.

Right-click on the schedulerpro folder and select SFDX: Deploy Source to Org. Create another Lightning Page with this updated Bryntum Scheduler Pro LWC. You now have a Bryntum Scheduler Pro LWC with CRUD functionality.

Arsalan Khattak

Bryntum Scheduler Pro Salesforce