CodeLive: Refactoring Data Driven Apps

Last week, Cynthia Thomas joined us for #CodeLive. We worked to refactor a bit of Apex code that clones cases. We refactored away from a Visualforce Controller to an @AuraEnabled Apex class, and built an LWC user interface for it. Missed the stream? We missed you too! Unfortunately, due to a technical glitch, there’s no recording this week. (Software is hard, lets go write software!) But never fear, we’ve vanquished webinar gremlins to the fixed bugs: section of the release notes, so next week’s stream will be recorded! Remember to register for future CodeLive streams here.

Our use case

It’s a bit of an unorthodox use case, but one that’s probably more common than you might think. Cynthia’s org allows customers to submit cases via email. Inside, Salesforce Email2Case creates a case from the incoming email. To the business, a case represents a single, actionable bit of work — for instance, ‘Annual Maintenance Check: Refrigeration Unit 1.’ Fortunately for the business, most customers have multiple refrigeration units. Unfortunately for the Email2Case setup, that means customers often email in a request for ‘Annual Maintenance Check for all my refrigeration units.’ The discrepancy between what constitutes a case can be handled in a number of ways, but for the stream, we decided to provide Salesforce users the ability to clone the incoming cases and edit their details.

Converting our Visualforce Controller Apex to a Lightning Web Components-compatible Apex class was simple enough — we were able to reuse the cloning logic. In fact, we could have refactored the existing Visualforce controller in place, but for clarity we started with a net-new class and ended up with this:

@AuraEnabled
public static List<Id> createCaseClones(Integer numberOfClonesToMake, Id caseIdToClone){
    Case toBeCloned = [Select ID FROM Case where ID =: caseIdToClone];
    
    list<Case> clonedCases = new List<Case>();
    for (Integer i=0; i < numberOfClonesToMake; i++){
        Case newClonedCase = new Case();
        newClonedCase = toBeCloned.clone();
        newClonedCase.parentId = caseIdToClone;
        clonedCases.add(newClonedCase);
    }
    
    insert clonedCases;
    Set<Id> theIds = new Map<id, Case>(clonedCases).keyset();
    return new List<Id>(theIds);
}

A single static method accepts the number of clones to make and the original case ID. It then clones the case the appropriate number of times. Our only change from the original code is in the addition of the parentId field. Populating this field allows each cloned case to relate back to the original case created by the customer. What’s important to note here is that the conversion from a Visualforce interface to a Lightning Web Components interface did not require a change in our actual cloning logic.

The UI

Our UI for this code has to do two things. First, we need to provide an interface for the user to specify the number of clones to make and a button to trigger their creation. Secondly, once our code has created the clones, we need a concise and easy way for the user to make appropriate edits to each individual clone. That way each clone represents an actionable, specific set of work items. We chose to build our UI in Lightning Web Components, because, well, they’re a lot of fun to develop. We also chose to mix custom and base components together to really deliver our UI in a quick and standard-look-and-feel.

Context

Our Lightning web component needs to understand its context so that, for instance, it knows what the current record id is. To do that we specified that the component can only be used on Record Detail Pages by adding a target to the component’s .js-meta.xml file.

<targets>
    <target>lightning__RecordPage</target>
</targets>

We also added an @api decorated property, called recordId. Together with the @api decorator, they allow the Lightning Web Components framework to automatically populate the recordId property with the ID of the current record. With our recordId sorted, we can move on to handling our user input.

Building user inputs with base components

Our component’s UI is built entirely with base components. Looking at the code below, you can see a Lightning Card provides the shape of the UI. The card’s actions slot holds a Lightning Layout with layout items consisting of Lightning-input and a lightning-button. This is what the code for the card and card’s header with actions looks like:

<lightning-card title="Clone this case into subcases">
    <div slot="actions">
      <lightning-layout horizontal-align="space">
        <lightning-layout-item padding="around-small">
          <div class="custom-box">
            <lightning-input
              variant="label-hidden"
              type="number"
              name="numberOfClonesToMake"
              placeholder="# of Clones - Max 30"
              onchange={handleCloneCountChange}
            >
            </lightning-input>
          </div>
        </lightning-layout-item>
        <lightning-layout-item padding="around-small">
          <div class="custom-box">
            <lightning-button
              label="Make Clones"
              onclick={handleButtonPress}
            ></lightning-button>
          </div>
        </lightning-layout-item>
      </lightning-layout>
    </div>
    ...

Note, how there’s an onchange handler on the lightning-input. This is how we update our controller code whenever the user changes the number of clones’ text field. Likewise, there’s an onclick handler assigned to the lightning-button. This allows us to attach logic to the user’s interaction with the button.

Calling our Apex method

Let’s look at how we call our Apex controller method from JavaScript:

  handleCloneCountChange(event) {
    this.numberOfClonesToMake = event.target.value;
  }

  handleButtonPress(event) {
    this.validateUserInput();
    // guard
    if (this.numberOfClonesToMake !== 0) {
      createCaseClones({
        numberOfClonesToMake: this.numberOfClonesToMake,
        caseIdToClone: this.recordId
      })
        .then(result => {
          this.clonedCaseIds = result;
          this.displayClonedCases = true;
          this.error = undefined;
        })
        .catch(error => {
          this.error = error;
          this.cloneCaseIds = undefined;
          this.displayClonedCases = false;
        });
    }
  }

Our first function handles whenever the user changes the value of our number of clones to make input field. You’ll notice it accepts a parameter, event, whose target.value property gives us the user input. Our second function is where things get interesting. Because we’re dealing with user input when the button is pressed, the first thing we do is validate the input numbers. In our case, this is a simple validation that the user has entered a number between 1 and 30. Once our user input is sorted, this code imperatively — or on-demand — calls a server side Apex method. How? Well at the top of our component, we added this line:

import createCaseClones from "@salesforce/apex/LWCCaseController.createCaseClones";

This import statement ties the LWCCaseController.createCaseClones method (on the right side of the from keyword) to the JavaScript object we named createCaseClones. (on the left side of the from keyword). Note, we could have named our JavaScript object anything, but keeping the method names identical helps make sense of the code.

Now that we have a JavaScript object, createCaseClones, we can execute it simply by calling it. If our method accepted no parameters, it’d be as simple as calling createCaseClones(), but since we do have parameters to pass in, we create a JSON object to hold them. It’s important to note that the keys in this JSON object must match the variable names in your Apex method.

Promises

Apex methods that we import are promise-enabled. This means two things: 1. They’re asynchronous. 2. We can tell the system to do some work, whenever the data returns. We do that with a .then() method call. In our case, we’re using a fat-arrow function to accept the result — which is passed in by the system when .then() is called — and setting our client-side variable this.clonedCaseIds equal to the result. Since our Apex code returns a List<Id> we know the result will be a List (Array) of IDs.

When things go as planned, the .then() method is called whenever the asynchronous work completes. However, when things go less than planned, the .catch() block is called. In our case, we’re capturing the error message(s) and making sure we don’t show any results. We could, however, retry the call or send a toast. The sky’s the limit here.

Promises give us flow control for our asynchronous work.

With our code setup to call Apex, which creates the clones and returns the IDs, we can now build the UI to display the new cases. This is where components in general — and the Lightning base components — really shine. Inside our template file, we put this code:

<template if:true={displayClonedCases}>
    <lightning-layout multiple-rows="true">
      <template for:each={clonedCaseIds} for:item="id">
        <lightning-layout-item padding="around-small" size="6" key={id}>
          <div>
            <lightning-record-form
              record-id={id}
              fields={fields}
              mode="view"
              object-api-name="Case"
              density="Compact"
              columns="1"
            ></lightning-record-form>
          </div>
        </lightning-layout-item>
      </template>
    </lightning-layout>
</template>

While this might look like quite a bit, it’s really just three elements:

  1. A sub-template that uses the if:true directive to conditionally show our newly cloned cases.
  2. A second sub-template that iterates over our collection of new case IDs.
  3. A Lightning Record Form base component. This base component needs only the record’s object name, its Id and an array of fields to show.

With just a handful of lines of code, we’ve defined a conditional UI displaying an editable form for each record that’s dynamically built from a list of fields. The base components and template directives allowed us to leverage platform functionality without writing any client or server side code to handle retrieving and modifying the record’s data!

Here’s what the final UI looks like in use.

This week’s CodeLive had some technical gremlins, but hey, that’s how you know it’s live! We took a data-driven use case for mass cloning case records and built a lovely (I’m biased) LWC user interface on top. We built with base components and we squeaked by in just under an hour! We put together a trailmix, along with this blog post to make sure those of you who couldn’t join in, could still benefit from the content. Take the Trailmix here.

Earlier this week, Salesforce Wizard Brian Kwong and I converted a public-facing Visualforce page into an LWC Lightning App — that recap will be coming soon. In the meantime, sign up for the next #CodeLive stream this #Codetober on October 24, where I will be reviewing code with Ryan Headley from Salesforce.org.

Leave your comments...

CodeLive: Refactoring Data Driven Apps