Code Sharing With Classes in LWC

by Aidan Harding - October 05, 2021
Code Sharing With Classes in LWC

There are some well-established methods for code sharing in LWC: We can compose entire UI components in the HTML template, or via Lightning App Builder. Or we can create a service component, and then export functions from that component (see the Developer Guide). But sometimes neither of those is appropriate

Sometimes the code we want to share requires state. In a language like Java or Apex, we would do this by encapsulating the functions and state into a class. That class could then be shared by composition or inheritance. I prefer composition to inheritance but LWC makes the choice for us anyway. It demands composition because UI components must extend LightningElement and Javascript does not support multiple inheritance (OK, mixins, but 🤮).

To investigate code sharing in LWC by composing classes, we take the example problem of sorting a datatable. Sorting like this has some state (the state stores which field is being sorted on, the sorting direction, etc.). It also needs to provide a method to handle the onsort event handler of lightning:datatable in the HTML template.

The obvious way doesn’t quite work

The obvious approach is to create a class that can do the sorting, while also keeping track of the current direction and field to be sorted on. Then, create an instance of that class inside the Javascript controller of your main LWC. In the HTML template of your LWC, you could bind the onsort method to the appropriate method in your class instance.

Here are some key sections of the code for that solution:

<template>
    <lightning-datatable
            key-field="id"
            columns={columns}
            data={data}
            hide-checkbox-column
            default-sort-direction={dataTableSorter.defaultSortDirection}
            sorted-direction={dataTableSorter.sortDirection}
            sorted-by={dataTableSorter.sortedBy}
            onsort={dataTableSorter.sortData}>
    </lightning-datatable>
</template>

sortedDatatable.html
// Setup data and columns first as constants for the demo
export default class SortedDatatable extends LightningElement {
    data = data;
    columns = columns;

    dataTableSorter = new DatatableSorter();
}

sortedDatatable.js
export class DatatableSorter {

    defaultSortDirection = 'asc';
    sortDirection = 'asc';
    sortedBy = null;
   
    // More things...
    
    sortData(event) {
        const { fieldName: fieldName, sortDirection } = event.detail;
        // Do the sorting...
    }
}

datatableSorter.js

Sadly, this does not quite work. The event handler is not called properly, so the sorting action has no effect.

The Problem

The problem is not specific to LWC. It’s a general property of how classes work in Javascript. When you call a method as “instance.method()”, then the method call has the “this” variable set to the object instance. If you store the method and call it later, then “this” is not set e.g.

const datatableSorter = new DatatableSorter();
const sortData = datatableSorter.sortData;

// Inside the call to "sortData", this is "datatableSorter" 
datatableSorter.sortData(); 

// Inside the call to "sortData", "this" is undefined
sortData(); 

When event handlers are invoked from an HTML template, LWC seems to solve this by setting “this” to be the “LightningElement” instance. Unfortunately for the example above, when “sortData()” is called, we need “this” to be the instance of “DatatableSorter”, not “SortedDatatable”.

The Solution

The solution is to create a new function that calls the method using “apply” and provides the correct value for “this”. For example:

const protectedSortData = function() {
    sortData.apply(datatableSorter, arguments);
}
// Inside the call to "protectedSortData", this is "datatableSorter" 
protectedSortData(); 

As long as datatableSorter was defined within the closure of protectedSortData, then this will work.

And, we can do this generically for any given class instance. We can overwrite all of its methods with versions that protect “this” using the closure.

function protectThis(objectInstance) {
    Object.getOwnPropertyNames( Object.getPrototypeOf(objectInstance) )
        .filter(maybeFunctionName => typeof objectInstance[maybeFunctionName] === 'function')
        .forEach(function(functionName) {
            const initialFunctionDefinition = objectInstance[functionName];
            objectInstance[functionName] = function()  {
                return initialFunctionDefinition.apply(objectInstance, arguments);
            }
        });
}

The above function can be called from the constructor of any class that will be used in the same way as DatatableSorter. Then, the methods of that class will be available to be used in the HTML template just as in our first attempt.

Tracking changes

Normally, changes to properties of the LightningElement that are referenced in the HTML template will cause the UI to rerender. This tracking of reactive properties only applies to direct properties in the LightningElement so changes inside the “datatableSorter” instance are not reactive. For this reason, and for separation of concerns, it makes sense to store the UI-bound data in the LightningElement and have your service component write to it there.

In this example, that would be DatatableSorter writing to SortedDataTable.data.

This is not hard to achieve by passing in the object instance and property name on construction i.e.

// Setup data and columns first as constants for the demo
export default class SortedDatatable extends LightningElement {
    data = data;
    columns = columns;

    dataTableSorter = new DatatableSorter(this, 'data');
}

sortedDatatable.js
export class DatatableSorter {

    defaultSortDirection = 'asc';
    sortDirection = 'asc';
    sortedBy = null;
   
    // More things...

    constructor(parent, dataFieldName) {
        protectThis(this);
        this.parent = parent;
        this.dataFieldName = dataFieldName;
    }
    
    sortData(event) {
        const { fieldName: fieldName, sortDirection } = event.detail;
        const cloneData = [...this.parent[this.dataFieldName]];
        // Do the sorting...
    }
}

datatableSorter.js

Conclusions

The most obvious method for code sharing in LWC is to simply break your component into smaller UI components. Then, they can be composed in the HTML template. This is the first choice when it is possible and appropriate.

We can also share pure Javascript code without any UI by creating a Service Component. In the service component, we are not limited to exporting functions, we can also export class definitions. Those class definitions can be extremely useful for logic that requires both state and methods.

If you want to use methods from the class in the service component as event handlers in an HTML template, then you can. But you must be careful about how the “this” reference is set. The “protectThis()” method above solves the problem associated with the “this” references, so it is a useful tool to have in your back pocket.

That last option of using “protectThis()” may be a niche concern. However, I hope that exploring the options has been useful, and understanding how this fits together will help you to make fine collections of cohesive components.

Repository

The full source code for this example is available here: https://github.com/aidan-harding/lwc-classes.

Feedback

As always, please do let me know what you think. Especially if you disagree or see any mistakes. Contact me on Twitter @AidanHarding or email aidan@nebulaconsulting.co.uk.

Related Content


Get In Touch

Whatever the size and sector of your business, we can help you to succeed throughout the customer journey, designing, creating and looking after the right CRM solution for your organisation