+ Start a Discussion
Brooks Johnson 6Brooks Johnson 6 

Sortable LWC

Hi Friends,

Far from a LWC expert here. and trying to get a Lightning Data Table that is sortable. I feel like I have followed everything in the documentation but when I click the arrow in the table header nothing happens. 

Here is my JS
import { LightningElement, track, wire, api } from 'lwc';
import getRecords from '@salesforce/apex/NewToLiftController.getRecords';
import { getRecord } from 'lightning/uiRecordApi';
import { NavigationMixin } from 'lightning/navigation';

const columns = [
    { label: 'First Name', fieldName: 'FirstName', type: "Text", sortable: true},
    { label: 'Last Name', fieldName: 'LastName', type: "Text", sortable: true },
    { label: 'Email', fieldName: 'Email', type: 'Email', sortable: true },
    { label: 'Role', fieldName: 'Role__c', type: 'Text', sortable: true },
    { label: 'Startup Leadership', fieldName: 'Startup_Leadership__c', type: 'boolean', sortable: true },
    { label: 'Company', fieldName: 'CompanyOrAccount', type: 'Text', sortable: true },
];

export default class DatatableBasic extends LightningElement {
@api recordId;
@track data = [];
@track columns = columns;
@track tableLoadingState = true;
@track rowOffset = 0;

@ wire(getRecords, {recordId: '$recordId'})
    wiredRecordsMethod({error, data}) {
        if (data) {this.data = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.data = undefined;
        }
        this.tableLoadingState = false;
    }
    increaseRowOffset(){this.rowOffset += 1;
    }
    // The method onsort event handler
    updateColumnSorting(event) {
        var fieldName = event.detail.fieldName;
        var sortDirection = event.detail.sortDirection;
        // assign the latest attribute with the sorted column fieldName and sorted direction
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        this.data = this.sortData(fieldName, sortDirection);        
    }
}
And here is the HTML
 
<template>
            <lightning-card title='First Time Campaign Memebers' icon-name="standard:contact">
                <lightning-datatable
                        key-field="id"
                        data={data}
                        show-row-number-column
                        row-number-offset={rowOffset}
                        hide-checkbox-column
                        columns={columns}
                        sorted-by={sortedBy}
                        sorted-direction={sortedDirection}
                        onsort={updateColumnSorting}
                        is-loading={tableLoadingState}>
                </lightning-datatable>
            </lightning-card>
</template>

 
Best Answer chosen by Brooks Johnson 6
Alain CabonAlain Cabon
@Brooks Johnson

Many differences in your code: Javascript is CASE SENSITIVE.

You have used:   
this.data = data_clone.sort(this.sortBy(fieldName), reverse);
                             sortby(field, reverse, primer) {

My correct solution was: this.data = data_clone.sort(this.sortBy(fieldName, reverse));
                                           sortBy(field, reverse, primer){
 
import {LightningElement, track, wire, api} from 'lwc';
import getRecords from '@salesforce/apex/NewToLiftController.getRecords';


const columns = [
    {label: 'First Name', fieldName: 'FirstName', type: "Text", sortable: true},
    {label: 'Last Name', fieldName: 'LastName', type: "Text", sortable: true},
    {label: 'Company', fieldName: 'CompanyOrAccount', type: 'Text', sortable: true},
    {label: 'Role', fieldName: 'Role__c', type: 'Text', sortable: true}
];

export default class DatatableBasic extends LightningElement {
@api recordId;
@track data = [];
@track columns = columns;
@track tableLoadingState = true;
@track rowOffset = 0;

@track sortedBy;
@track sortedDirection;

@wire(getRecords, {recordId: '$recordId'})

    wiredRecordsMethod({error, data}) {
        if (data) {
            this.data = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.data = undefined;
        }
        this.tableLoadingState = false;
    }

    increaseRowOffset() {
        this.rowOffset += 1;
    }

    // The method onsort event handler
    updateColumnSorting(event) {
        let fieldName = event.detail.fieldName;
        let sortDirection = event.detail.sortDirection;
        // assign the latest attribute with the sorted column fieldName and sorted direction
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        console.log('Sort fieldName: ' + fieldName);
        console.log('sort direction: ' + sortDirection);

        let reverse = sortDirection !== 'asc';
       
        let data_clone = JSON.parse(JSON.stringify(this.data));

        console.log('BEFORE data_clone:' + JSON.stringify(data_clone));
       
        this.data = data_clone.sort(this.sortBy(fieldName, reverse));

        console.log('AFTER data_clone:' + JSON.stringify(data_clone));
       
    }

    sortBy (field, reverse, primer){

        console.log('Sort by:reverse:' + reverse);

        var key = function (x) {return primer ? primer(x[field]) : x[field]};
     
        return function (a,b) {
            var A = key(a), B = key(b);

            if (A === undefined) A = '';
            if (B === undefined) B = '';
        
            return (A < B ? -1 : (A > B ? 1 : 0)) * [1,-1][+!!reverse];                  
        }
    }
}


The detailed verification of the upper case, lower case should be done ALWAYS very carefully to avoid many problems in Javascript.

The error messages in javascript are awful and not very precise most of the time when don't take care of this rule about the case sensitivity.

All Answers

Alain CabonAlain Cabon
@Brook Johnson

The documentation is really incomplete and needs at least one workaround.

I struggled a lot but that seems to work.

The workaround is this one: 

let data_clone = JSON.parse(JSON.stringify(this.contacts));   //  it is a clone of the data that will be sorted.
this.contacts = data_clone.sort(this.sortBy(fieldName, reverse));

You must provide the algorithm of sort (incomplete documentation) for LWC but complete for Aura Lightning (I know it much better). 
 
<template>
    <lightning-card title='First Time Campaign Memebers' icon-name="standard:contact">
        <div class="slds-p-around_medium lgc-bg" style="height: 300px;">
        <lightning-datatable
                key-field="Id"
                data={contacts}
                columns={columns} 
                sorted-direction={sortedDirection} 
                sorted-by={sortedBy}
                onsort={updateColumnSorting}
                >
        </lightning-datatable>
        </div>
    </lightning-card>
</template>
 
public with sharing class NewToLiftController {
   
     @AuraEnabled(cacheable=true)
     public static List<Contact> getRecords() {
        
        List<Contact> lc = [select Id, FirstName,LastName from Contact where firstname != null and lastname != null LIMIT 10];
        system.debug('getRecords contact:' + lc.size());
        return lc;
    }
}
 
import {
    LightningElement,
    track,
    wire
} from 'lwc';


import getRecords from '@salesforce/apex/NewToLiftController.getRecords';

const columns_init = [{
        label: 'First Name',
        fieldName: 'FirstName',
        type: "text",
        sortable: true
    },
    {
        label: 'Last Name',
        fieldName: 'LastName',
        type: "text",
        sortable: true
    }
];

const contacts_init = [{
        'Id': '1',
        'FirstName': 'toto',
        'LastName': 'titi'
    },
    {
        'Id': '2',
        'FirstName': 'lulu',
        'LastName': 'lili'
    }
];

export default class DatatableBasic extends LightningElement {

    @track contacts = contacts_init;
    @track columns = columns_init;

    @track sortedBy = 'FirstName';
    @track sortedDirection = 'asc';

    @track error = null;

    @wire(getRecords)
    wiredContacts({
        error,
        data
    }) {
        if (data) {
            this.contacts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
        }
    }

     // The method onsort event handler
     updateColumnSorting(event) {
        let fieldName = event.detail.fieldName;
        let sortDirection = event.detail.sortDirection;
        // assign the latest attribute with the sorted column fieldName and sorted direction
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        console.log('sort fieldName:' + fieldName);
        console.log('sort direction:' + sortDirection);
        let reverse = sortDirection !== 'asc';
        let data_clone = JSON.parse(JSON.stringify(this.contacts));
        this.contacts = data_clone.sort(this.sortBy(fieldName, reverse));
     }

     sortBy(field, reverse, primer){

        var key = primer ? 
            function(x) {return primer(x[field])} : 
            function(x) {return x[field]};
     
        reverse = !reverse ? 1 : -1;
     
        return function (a, b) {
            return a = key(a), b = key(b), reverse * ((a > b) - (b > a));
        } 
     }

}
 
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="NewToLift">
    <apiVersion>45.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
      <target>lightning__AppPage</target>
      <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

 
Brooks Johnson 6Brooks Johnson 6
Thanks as always Alain. I will give this a try. 
Brooks Johnson 6Brooks Johnson 6
Alain, I tried to repurpose your code. But my table is throwing an error when I try to sort. Not sure what might be the cause. 

Here is the updated JS
import {LightningElement, track, wire, api} from 'lwc';
import getRecords from '@salesforce/apex/NewToLiftController.getRecords';
import {getRecord} from 'lightning/uiRecordApi';
import {NavigationMixin} from 'lightning/navigation';

const columns = [
    {label: 'First Name', fieldName: 'FirstName', type: "Text", sortable: true},
    {label: 'Last Name', fieldName: 'LastName', type: "Text", sortable: true},
    {label: 'Company', fieldName: 'CompanyOrAccount', type: 'Text', sortable: true},
    {label: 'Role', fieldName: 'Role__c', type: 'Text', sortable: true}
];

export default class DatatableBasic extends LightningElement {
@api recordId;
@track data = [];
@track columns = columns;
@track tableLoadingState = true;
@track rowOffset = 0;

@wire(getRecords, {recordId: '$recordId'})

    wiredRecordsMethod({error, data}) {
        if (data) {
            this.data = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.data = undefined;
        }
        this.tableLoadingState = false;
    }

    increaseRowOffset() {
        this.rowOffset += 1;
    }

    // The method onsort event handler
    updateColumnSorting(event) {
        let fieldName = event.detail.fieldName;
        let sortDirection = event.detail.sortDirection;
        // assign the latest attribute with the sorted column fieldName and sorted direction
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        console.log('Sort fieldName: ' + fieldName);
        console.log('sort direction: ' + sortDirection);
        let reverse = sortDirection !== 'asc';
       /* this.data = this.sortData(fieldName, sortDirection);
        this.data = JSON.parse(JSON.stringify(this.data));*/
        let data_clone = json.parse(JSON.stringify(this.data));
        this.data = data_clone.sort(this.sortBy(fieldName), reverse);
    }

    sortby(field, reverse, primer) {
        let key = primer ?
            function (x) {return primer(x[field])} :
            function (x) {return [field]};
        reverse = !reverse ? 1 : -1;

        return function (a, b) {
            return a = key(a), b = key(b), reverse * ((a > b) - (b > a));
        }

    }
}

And here is the HTML
 
<template>
            <lightning-card title='First Time Campaign Members' icon-name="standard:contact">
                <lightning-datatable class="slds-table slds-table--bordered slds-table--striped slds-table--col-bordered slds-table--resizable-cols"
                        key-field="id"
                        data={data}
                        show-row-number-column
                        row-number-offset={rowOffset}
                        hide-checkbox-column
                        columns={columns}
                        sorted-by={sortedBy}
                        sorted-direction={sortedDirection}
                        onsort={updateColumnSorting}
                        is-loading={tableLoadingState}>
                </lightning-datatable>
            </lightning-card>
</template>

 
Alain CabonAlain Cabon
We need to verify all your code from the beginning.

Did you use the devtools of Chrome ( Ctrl+Shift+I ).
https://developers.google.com/web/tools/chrome-devtools/

Why is there import "getRecord" ? 
import {getRecord} from 'lightning/uiRecordApi';
Where is the wired part for: @wire(getRecord ... ?

Where is the Apex class? It is a campaign member. I must also verify your SOQL query.
Alain CabonAlain Cabon
For the sorting algorithm, there are many alternatives.

Problem of the "undefined" values:
sortby (field, reverse, primer){

        var key = function (x) {return primer ? primer(x[field]) : x[field]};
     
        return function (a,b) {
            var A = key(a), B = key(b);

            if (A === undefined) A = '';
            if (B === undefined) B = '';
        
            return (A < B ? -1 : (A > B ? 1 : 0)) * [1,-1][+!!reverse];                  
        }
 }

 
Alain CabonAlain Cabon
For me, that works always really fine. I need to check your code.
Alain CabonAlain Cabon
There are many missing parts and errors (or just out of date samples) in the LWC documentation. I am currently testing these samples.

So I am very interesting by your code. There is a useless part in your code but I would like to see another error likely in your Apex class.

import {getRecord} from 'lightning/uiRecordApi';

 
Alain CabonAlain Cabon
I need to see your Apex code because you probably don't use an annoted Apex method with cacheable=true.

https://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.apex

That is one the check and corrections that must be done in your defective code (that is not my code clearly that you have used). 
Brooks Johnson 6Brooks Johnson 6
Hi Alain, 

Below is the Apex class. It is aura enabled and cacheable=true. Everything is fine until I sort then it throws an error on the page. 
 
/**
 * @author Brooks Johnson -Athena Global Advisors
 * @DATE 4 June 2019
 * @DESCRIPTION controller for LWC
 */

public with sharing class NewToLiftController {
	
	@AuraEnabled(Cacheable = true)
	public static List<CampaignMember> getRecords(String recordId) {
		List<CampaignMember> campaignMembers = [
				SELECT FirstName, LastName, Role__c,  CompanyOrAccount
				FROM CampaignMember
				WHERE NewToLift__c = TRUE
				AND CampaignId = :recordId
		];
		return campaignMembers;
Alain CabonAlain Cabon
@Brooks Johnson

Many differences in your code: Javascript is CASE SENSITIVE.

You have used:   
this.data = data_clone.sort(this.sortBy(fieldName), reverse);
                             sortby(field, reverse, primer) {

My correct solution was: this.data = data_clone.sort(this.sortBy(fieldName, reverse));
                                           sortBy(field, reverse, primer){
 
import {LightningElement, track, wire, api} from 'lwc';
import getRecords from '@salesforce/apex/NewToLiftController.getRecords';


const columns = [
    {label: 'First Name', fieldName: 'FirstName', type: "Text", sortable: true},
    {label: 'Last Name', fieldName: 'LastName', type: "Text", sortable: true},
    {label: 'Company', fieldName: 'CompanyOrAccount', type: 'Text', sortable: true},
    {label: 'Role', fieldName: 'Role__c', type: 'Text', sortable: true}
];

export default class DatatableBasic extends LightningElement {
@api recordId;
@track data = [];
@track columns = columns;
@track tableLoadingState = true;
@track rowOffset = 0;

@track sortedBy;
@track sortedDirection;

@wire(getRecords, {recordId: '$recordId'})

    wiredRecordsMethod({error, data}) {
        if (data) {
            this.data = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.data = undefined;
        }
        this.tableLoadingState = false;
    }

    increaseRowOffset() {
        this.rowOffset += 1;
    }

    // The method onsort event handler
    updateColumnSorting(event) {
        let fieldName = event.detail.fieldName;
        let sortDirection = event.detail.sortDirection;
        // assign the latest attribute with the sorted column fieldName and sorted direction
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        console.log('Sort fieldName: ' + fieldName);
        console.log('sort direction: ' + sortDirection);

        let reverse = sortDirection !== 'asc';
       
        let data_clone = JSON.parse(JSON.stringify(this.data));

        console.log('BEFORE data_clone:' + JSON.stringify(data_clone));
       
        this.data = data_clone.sort(this.sortBy(fieldName, reverse));

        console.log('AFTER data_clone:' + JSON.stringify(data_clone));
       
    }

    sortBy (field, reverse, primer){

        console.log('Sort by:reverse:' + reverse);

        var key = function (x) {return primer ? primer(x[field]) : x[field]};
     
        return function (a,b) {
            var A = key(a), B = key(b);

            if (A === undefined) A = '';
            if (B === undefined) B = '';
        
            return (A < B ? -1 : (A > B ? 1 : 0)) * [1,-1][+!!reverse];                  
        }
    }
}


The detailed verification of the upper case, lower case should be done ALWAYS very carefully to avoid many problems in Javascript.

The error messages in javascript are awful and not very precise most of the time when don't take care of this rule about the case sensitivity.
This was selected as the best answer
Brooks Johnson 6Brooks Johnson 6
Perfect Alain, thank you so much for your help. 
Alain CabonAlain Cabon
@Brooks Johnson

The sorting algorithms are anything but trivial even more in javascript. It is a puzzle in order to find the shortest code or the fastest one.
The proof of these sorting codes is a complex problem but we know that these solutons work (common used solutions).
Given that we sort small data table, the fastest sorting algorithm is not important (the response is still immediate here).

For sorting a field with numbers, a new improved version of the algorithm could be better probably.

Sorting an array of JavaScript objects by property (you will find the solutions above and Salesforce also use them in their documentation)
https://stackoverflow.com/questions/979256/sorting-an-array-of-javascript-objects-by-property

... it is an endless thread on developer forums (best way of sorting an array of JS objects by a field, many solutions).
Alain CabonAlain Cabon
To be more precise, sortby (field, reverse, primer) is not the complete sorting algoritm itself but just the part that compares two values and returns the indication for the order between these two values of the column sorted according the ascending or descending order.

data_clone.sort ()  : it is the default sort of javascript already optimized.
 
ericmonteericmonte
@Alain thank you answer this. I was trying to figure out the sorting for couple hours and realized the documentation in LWC is not completed. Your answer helped a lot and now I can sort my datatable. 
Alain CabonAlain Cabon
@Eric   You are welcome. 

There are sophitiscated examples for the lightning-datatable but no one is sortable.
So we looked at the documentation tab, not bad at all but a partial example for a sortable datatable (?) so the struggle began. 

https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/example

They also used their own data-faker (work in the playground) but not sure we can see the source code (not ideal for basic examples).
return fetch('https://data-faker.herokuapp.com/collection', {

If you improve the  sortBy (field, reverse, primer) (for numbers for instance), do not hesitate to post a new solution. 

Best regards
Alain