announcing bolt 1.0 for lwc

Today I'm announcing to the public Bolt for LWC. A library that I've been developping since almost 6 months now, as I worked on a deeply front-end logic centered Salesforce Experience Cloud App. The library has inherited from certain characteristics that are intricated with the state of the project I worked on was at that time (more on that later).

Isn't LWC enough to make front-end intensive reactive apps in Salesforce ?

Well, to a certain extent yes. But in reality, when your app starts to grow in size, when you have multiple components with repeating logic, when you feel exhausted of rewriting the same statement over and over just to access one SObject and so on.. you actually feel quit limited by the standard capabilities of LWC.

Ok, then what other choice do we have ? I have 2 in mind :

Our client wasn't willing to pay for heroku, so second option was out of the window and let's face it, trying to integrate React or whatever framework in such a claustrophobic environment that is Salesforce, is like shooting you in the foot before you even began to write a single line of code.

So, that's how, bit by bit, as I worked on this real-world application and with the complexity surrounding it, I came up with small utility functions here and there, that, piled up, ended up making a whole library, namely, Bolt.

Eliminate boilerplate and get straight to the point

The main issue I had when I learned LWC (and that I still have), was that to use the salesforce preferred way of retrieving data in the context of an LWC, that is using Lightning Data Service, you have to write so many boilerplate code that, if you have to write one component every month, that's ok, but when you're dealing with a codebase of 20+ LWC, you're starting to reconsider life decisions. I wrote about this exact topic a while ago.

So, one of the starting point of Bolt was this very issue : reducing boilerplate to its bare minium while abstracting best practices.

To achieve that, Bolt is heavily relying on the concept of Mixins.

Using latest APIs

Bolt is relying on the latest LWC available APIs and therefore requires your org to be on LightningWebSecurity to be fully functionnal.

Words are good, but let's see it in action.

Let met give you the context :

  1. We're building a search form to query data on an external api (that somehow we'll reach through Apex)
  2. Once the data back, we want to let the user alterate them the way to want to create a salesforce record based on those received values.
  3. Finally, an external system should be aware of the creation of this new record in Salesforce and should calculate some amounts for it.
    1. So another webservice call has to be made upon saving the record
    2. The client should be listening for the response that will be sent over a PlatformEvent.

Search Form

 1import {track, LightningElement} from 'lwc';
 2import {
 3  mix,
 4  useFormValidation,
 5  useReactiveBinding,
 6  useState
 7} from 'c/bolt';
 8import calloutInApex from '@salesforce/apex/controller.calloutMethod';
 9export default searchForm extends mix(
10  [useFormValidation],
11  [useReactiveBinding],
12  [useState, {IDLE: true, SEARCHING: false, RECEIVED: false, ERROR: false}],
13  LightningElement
14) {
15  @track formData = {
16    contractNumber: '',
17    clientName: '',
18    mileage: ''
19  };
20  @track apiResponse;
21  async handleCalloutExternalAPI() {
22    if(this.isFormValid) {
23      this.SEARCHING = true;
24      this.apiResponse = await calloutInApex(this.formData)
25        .catch(err => this.ERROR = true);
26        // some kind of latency
27      if(this.apiResponse)
28        this.RECEIVED = true;
29    }
30  }
31}
 1<template>
 2  <template lwc:if={IDLE}>
 3    <lightning-input
 4      data-checkable
 5      data-bind="formData.contractNumber"
 6      value={formData.contractNumber}
 7      label="Contract number"
 8      onchange={bind}
 9    ></lightning-input>
10    <lightning-input
11      data-checkable
12      data-bind="formData.clientName"
13      value={formData.clientName}
14      label="Client name"
15      onchange={bind}
16    ></lightning-input>
17    <lightning-input
18      data-checkable
19      data-bind="formData.mileage"
20      value={formData.mileage}
21      label="Mileage"
22      onchange={bind}
23    ></lightning-input>
24    <lightning-button 
25      label="Search" 
26      onclick={handleCalloutExternalAPI}
27    ><lightning-button/>
28  </template>
29  <template lwc:elseif={SEARCHING}>
30    <c-bolt-skeleton rows="5"></c-bolt-skeleton>
31  </template>
32  <template lwc:elseif={RECEIVED}>
33    <c-record-creation-form 
34      lwc:spread={apiResponse}
35    ></c-record-creation-form>
36  </template>
37  <template lwc:else>
38    <!--ERROR CASE-->
39  </template>
40</template>

In this, rather long but good introduction, of Bolt we can see the use of

useReactiveBinding lets you bind together one property of your class to one input, just by adding [data-bind] to the input and by referencing the bind() method to the onchange handler of the input.

useFormValidation provides you with a single getter isFormValid that returns a boolean and will report validity on the form's input if any errors are found.

useState lets you declare all the different states you lwc can be in, letting you adapt the view accordingly

is just an infinite loading bar component that can be used in place of a . It is fully customisable through css custom properties.

Record Creation Form

 1import {
 2  useForm,
 3  useDML,
 4  mix,
 5  BoltElement
 6} from 'c/bolt';
 7import caseFields from './caseFields.js';
 8import contactFields from './contactFields.js';
 9import secondCalloutInApex from '@salesforce/apex/controller.secondCalloutMethod';
10export default class recordCreationForm extends mix(
11  [useForm, [caseFields, contactFields], 'insert'],
12  [useDML],
13  BoltElement
14) {
15  async handleSaveAndCallout() {
16    if(this.refs.form.validity) {
17      const [case, contact] = await this.saveRecords([this.Case, this.Contact])
18        .catch(err => errorHandlingSomehow(err));
19      if(case.Id && contact.Id) {
20        const request = buildRequestSomehow(case, contact);
21        const response = await secondCalloutInApex(request);
22      }
23    }  
24  }
25  get records() { return [this.$Case, this.$Contact] }
26 }
1<template>
2  <c-bolt-form record={$Case} mode='insert' lwc:ref="form">
3  </c-bolt-form>
4  <lightning-button label="Save the record" onclick={handleSaveAndCallout}>
5  </lightning-button>
6</template>

Here we can see the use of

useForm accepts 2 parameters that are of type Field[][] and string, where Field is what you get when you import a field like this import ContactName from "@salesforce/schema/Contact.Name";.

Because here, we're importing a bunch of fields from both the Case and Contact object, our class will be augmented with several new reactive properties :

Case and Contact props are simple JS objects whose keys will be the apiName of the imported fields and as the value their respective values. In this example, because the form is in insert mode, all fields will be assigned an empty default value.

The $ prefixed props are the ones holding records information (dataType, label, picklist values ...) as well as a reference to the previous properties containing the data.

useDML simply provides utility methods to save or delete a single or a bunch of records.

BoltElement is the glue that holds this complex logic together.

is a helper component that relies on Bolt's <c-bolt-input /> component to display in a grid all of the fields passed as attribute. It ensures data binding of any changes you made to an input.

Listening for a PlatformEvent in an Experience Cloud website

Now let's add a third mixin to this component that will gives us the ability to listen for an update caused by the reception of a PlatformEvent.

Assuming in the request that precedes the PlatformEvent you send the Id of the record that should be updated upon receiving the PE

 1import {
 2  useForm,
 3  useDML,
 4  usePoller,
 5  mix,
 6  BoltElement
 7} from 'c/bolt';
 8import caseFields from './caseFields.js';
 9import contactFields from './contactFields.js';
10import secondCalloutInApex from '@salesforce/apex/controller.secondCalloutMethod';
11import getUpdatedRecord from '@salesforce/apex/controller.getUpdatedRecord';
12export default class recordCreationForm extends mix(
13  [useForm, [caseFields, contactFields], 'insert'],
14  [useDML],
15  [usePoller, {
16    resolveCondition: results => results.status__c === 'WELL_RECEIVED',
17    wiredMethod: 'updatedRecord',
18    maxIteration: 60,
19    interval: 1000
20  }],
21  BoltElement
22) {
23  @track caseId;
24  connectedCallback() {
25    super.connectedCallback();
26    this.template.addEventListener('polling-end', (detail:{status, response}) => {
27      if(status === 'OK') // doSomething
28      if(status === 'POLLING_LIMIT_EXCEEDED') // doSomething
29    })
30  }
31  @wire(getUpdatedRecord, {recordId: '$caseId'})
32  updatedRecord;
33  async handleSaveAndCallout() {
34    if(this.refs.form.validity) {
35      const [case, contact] = await this.saveRecords([this.Case, this.Contact])
36        .catch(err => errorHandlingSomehow(err));
37      if(case.Id && contact.Id) {
38        this.caseId = case.Id;
39        this.initPoller();
40        const request = buildRequestSomehow(case, contact);
41        const response = await secondCalloutInApex(request);
42      }
43    }  
44  }
45  get records() { return [this.$Case, this.$Contact] }
46 }

Here we listen for the 'polling-end' event than can be fired with two values possible for the status parameter

Future

For the V1, Bolt provides :

A great addition I'm planning on working on, is type-safe mixins to improve a bit the DX. Because right now whenever you'll use a method or property injected by a mixin, VSCode for example, won't recognize it. So stay tuned for that !

Thanks.

2024-06-22 ABC

Good

2024-06-21 ertr

erte

2024-06-21 ertr

erte

2024-06-21 ertr

erte

2024-06-19 Abdelhafid

dadada

2024-06-18 ww

ww