BlogSalesforce

Tutorial: Upgrade a Salesforce Aura Component into Lightning Web Component (LWC)

By January 7, 2021 January 21st, 2021 No Comments

Salesforce has made the switch to Lightning Web Components (LWC) from the older Aura framework. While Aura components have helped many developers easily build custom features on the platform, Salesforce is evolving with faster, more secure LWC components. In this blog, I’m going to show how to convert an existing Aura component into an LWC one to ensure your investment keeps pace.

Converting an Aura Component to an LWC Component

My original Aura component is still functional and can be accessed on Github.

My new component can be accessed there as well.

The new one is very similar except that I’ve added a double click to move feature, which speeds up selections a LOT. It still has the original functionality of drag to move, reorder, and arrows to move and reorder.

Here’s how it looks:

Aura and LWC are actually quite different, but there are still some easy wins when we make the conversion.

  • HTML markup is easily changed – it’s the same structure, with different bindings. The main change is “{!v.yourVar}” becomes {yourVar} Nicer ,eh!
  • CSS is also mostly the same. Just remove and .THIS references, and it’ll pretty much work.
  • CSS use of the SLDS also works – as long as it’s in the Salesforce ecosystem, you don’t need to do anything.
  • In the JavaScript file (which takes the place of the Aura Controller and Helper files), your init callback is replaced by a connectedCallback method.

Things get harder when you want to share data between components or within the component. You also can’t over-style a built-in SLDS component, which is probably for the best.

  • Bindings are one-way – once the data is set in your HTML, you need to handle any changes via an event listener.
  • Almost no logic can be performed in the HTML bindings – the days of crazy logic statements (which had already been limited in Aura compared to Visualforce) are gone. Use a getter method instead. You’ll thank yourself and Salesforce for doing this because now the logic lives in one place – your JS file.
  • Essentially, data flows down, and events bubble up (or capture up).

Ok, let’s get started on the component.

Former Aura Component Structure

My old component had a structure like this:

Container 
 -Dualselect (handling move events mostly)
   -OrderedList (left)
   -OrderedList (right)

This has not changed with the LWC component – I still abstract away the lists into two separate list components which you could use independently if you wanted (they might not be that useful).

LWC Structure

First, the old ordered list (here’s a cut down version of the attribute/event markup):

    <!--event handlers--> 
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <!--event registration--> 
    <aura:registerEvent name="multiColumnSelectChange" type="c:ComponentEvent" />
    <!--public methods-->  
    <aura:method name="removeItem" action="{!c.deleteFromList}" description="Delete an item based on a unique key">
        <aura:attribute name="itemId" type="String" access="public"  />
    </aura:method>
    <aura:method name="moveItems" action="{!c.moveItems}" description="move items from this list to another"></aura:method>
    <!--public attributes-->
    <aura:attribute name="fieldName" type="String" access="public"  />
    <aura:attribute name="values" type="Object[]" description="all available values for this field" access="public"  />
    <aura:attribute name="position" type="String" description="left or right" access="public"  />
    <!--private attributes-->
    <aura:attribute name="uuId" type="String" access="private"  />
    <aura:attribute name="highlightedItem" type="Object" description="an item ready to be transferred to the selectedValues array" access="private" />

the JavaScript, and there’s no clumsy component.get(“v.variable”); syntax – you just refer to all variables in the controller class as a class member using this.variable. Easy as!

Here’s what the above is replaced by in the LWC app JavaScript controller:

//takes place of init handler above
connectedCallback() {
  this.uuid = this.uniqueId();
}

//no need to register events anymore

//same as remove item above
@api
deleteItem(itemId) {
  this.items = this.removeItem(itemId, this.items);
}

//replaces "moveItems" above
@api
moveItemsAway() {
  this.items = this.removeItems(this.highlightedItems, this.items);
  this.highlightedItems = [];
}

//replaces the public attributes above
@api fieldName = '';
@api showUpDown = false;
@api position = LEFT; //can be left or right
@api
set values(values) {
  if (!values || values == undefined) {
    this.items = [];
    return;
  }
  console.log(JSON.parse(JSON.stringify(values)));
  this.items = JSON.parse(JSON.stringify(values));
  this.initializeList();
}
get values() {
  return this.items;
}

//finally private attributes:
@track items = [];
@track uuid = '';

As you can see, the second LWC list looks longer, but it is replacing code from three possible areas in the Aura app – the .cmp file, the Controller.js, and the Helper.js file. So, in reality, it’s reducing the amount of code you need to write to get the job done. Plus, it was never well-defined in an Aura app what should go in the controller and what should go in the helper file. There’s only one place in an LWC app (unless you define a special helper file and import that).

Aura Component Bindings

Aura bindings look like (1):

<span class="slds-form-element__label" >{!v.fieldName}</span>

They can include expressions such as (2):

<li class="{! ' slds-listbox__item ' + item.class}"

The equivalent in LWC would be (1):

<span class="slds-form-element__label" >{fieldName}</span>

And the expression (and attribute) syntax (2):

<li style={listItemStyle}> or <li class={item.class}>

//JS: 
get listItemStyle(){
  return slds-listbox__item ${this.someStyle};
}

Essentially for binding, the idea is to keep as much logic out of the markup and put it into the Javascript file. This makes the HTML much more readable and less dynamic, which is part of the reason for the speed advantage that LWC has over Aura.

CSS

Lightning Web Component CSS is mostly the same – except for a few interesting differences.

The most notable difference is that CSS doesn’t really cascade anymore. It applies only to your component and any components that it has been imported into.

The LWC environment includes SLDS in every component, so that is not a problem. All these styles will work. However, in Aura, it was possible to override built-in component styles. This is not possible in LWC, so if you want something radically different, you will need to build it yourself.

If you are overriding an SLDS style on the markup of the actual component you are building, then that is possible.
i.e.

.slds-form__row .slds-form__item  {
   margin-bottom: 20px;
}

Will apply to:

<div class="slds-form__row">
        <div class="slds-form__item" role="listitem">

As long as it’s YOUR markup.

Drag and Drop

Drag and drop has some differences, mostly driven by the way LWC are structured (knowledge of parent/children is only via events and API).

Aura Component Method

handleOnDropParent: function(component, event) {
    
    event.preventDefault();
    event.stopPropagation();
    
    //fix to prevent duplication of items when dragged onto the same list
    var targetOption = event.target;
    if (targetOption !== undefined) {
            while (targetOption !== undefined && targetOption.tagName !== 'li' && targetOption.tagName !== 'LI') {
            targetOption = targetOption.parentElement;
        }
    }
    var items = component.get("v.items");
    var rawData = event.dataTransfer.getData('text');
    var item = JSON.parse(rawData);
    var receivingItem = this.getItem(targetOption.id,items);
    item = this.getItem(item.id,items) || item;

LWC way

handleOnDropParent: function(component, event) {

event.preventDefault();
event.stopPropagation();
//prevents a null pointer error, because sometimes in LWC this event is fired without any dataTransfer data.
if (event.dataTransfer == null){
  return;
}
let targetElementId = event.target.dataset.id;
let parentHandlingEvent = (targetElementId == this.uuid);
let rawData = event.dataTransfer.getData('text');
let item = JSON.parse(rawData);
let droppedOnSelf = (item.parentId == this.uuid);
let receivingItem = this.getItem(targetElementId, this.items);
item = this.getItem(item.id, this.items) || item;

As you can see, there are a few more hoops to jump through in LWC (again related to the inability of a parent component to interrogate its child components – except via an API), which means the code is a little more verbose, but it all works well.

Most of the other drag and drop methods are similar to each other (see the GitHub repo for more examples)

Events

I used a mix of separately defined Application Events and Component events in the Aura app, and these cluttered up the project. The new event syntax is nice to use and more succinct to define. Something similar to Application events is possible (you can leverage a built-in event framework), but I’ve preferred to use the LWC equivalent of component events.

To make this work, I have structured the component as a parent with two separate lists (left and right) inside it. When an event happens on one side, it broadcasts an event to the parent, which handles it and calls an API method on one or both of the child lists.

In the Aura app, I defined an Application event in each ordered list:

<aura:handler event="c:ApplicationEvent" action="{!c.handleDataChangeAppEvent}"/>

When a change occurred, each of the two lists could handle changes mostly independently of the parent component. This is quite inefficient and requires that the Aura runtime handle a lot of Application events, which actually resulted in some performance problems.

In LWC, being all component (ish) events, only the parent handles events.

They are dispatched like this:

dispatchMoveItems = () => {
  const moveitems = new CustomEvent('moveitems', {
    detail: { uuid: this.uuid, items: this.highlightedItems }
  });
  this.dispatchEvent(moveitems);
}

And handled like this (in the onmoveitems handler):

<c-ordered-list class="left" field-name={fieldName} values={leftValues_} position="left" 
                    ondragcomplete={handleDragCompleteLeft} onmoveitems={moveLeftToRight}>
</c-ordered-list>

General JavaScript

For the most part, most of the internal list movement and sorting JavaScript was able to remain much the same, as I had already abstracted it in the Aura component.

For example, this method, used in both Aura and LWC implementations, just takes a list of items and returns the resulting modified list – I used this kind of thing all over the component.

removeItems = (itemsToRemove, items)=> {
  itemsToRemove.forEach( (itemToRemove)=> {
    items = items.filter( (item)=> {
      return item.id !== itemToRemove.id;
    });
  });
  return items;
}

Transitioning an Aura Component

Moving from an Aura component to an LWC is not straightforward. However, I encourage you to make the effort, considering the speed of your new components, the ease of calling Apex/Wire methods (not covered in this blog), and the new direction of Lightning components. New Salesforce components will be LWC, and Aura components face eventual deprecation.

A short checklist for conversion:

  • Copy raw HTML
  • Convert data bindings.
  • Convert any logic in HTML to getters in the JavaScript file.
  • Check any CSS overrides.
  • Pull logic from helper and controller classes into a single js file.
  • Convert events from Component and Applications events to CustomEvents.
  • Transition any Apex calls to LWC Apex Imperative or Wire calls.
  • Convert any recordView or recordEdit components to use the new versions.
  • Change any Resource imports to new Syntax.
  • Check exposure of your Aura component to Lightning pages or Tabs etc. You can now do this via the LWC meta.xml file.

In this blog, I focused on converting a particular Aura component to LWC, so I did not address Resource imports and Server-side Aura calls (Wire or Imperative methods).

Download the Files

All the files referenced here are located on GitHub.

Enjoy!

Moving Forward in Lightning

Have questions? Let us know in a comment below, or contact our team directly. You can also check out our other posts on customizing your Salesforce solution.

Caspar Harmer

Caspar Harmer

Caspar is a New Zealander working for Soliant from far-off Wellington. He loves exploring new technologies and solving problems. Caspar also loves getting into the outdoors; he runs, mountain bikes and does a lot of orienteering when he can fit it in.

Leave a Reply