+ Start a Discussion
Christian Schwabe 9Christian Schwabe 9 

LWC: Datatable - preselect rows programmatically

Hi all,

in the documentation (https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/documentation) a way is documented to preselect rows programmatically.
For this reason you should use "selected-rows"-attribut on lightning-datatable.

DOCUMENTATION

The selected-rows attribute enables programmatic selection of rows, which is useful when you want to preselect rows.
<lightning-datatable
    columns={columns}
    data={data}
    key-field="id"
    selected-rows={selectedRows}>
</lightning-datatable>
<lightning-button
    label="Select"
    onclick={handleSelect}>
</lightning-button>

To select a row programmatically, pass in the row key-field value.
// Load data via init handler first
// then handle programmatic selection
handleSelect() {
   const rows = ['a'];
   this.selectedRows = rows;
}

My component looks (simplfied) the following way:
export default class MergeDuplicates extends LightningElement {
    @track preSelectedRows = ['0011x00000KOMiJAAX'];
    
    [...]
}

My html-file (simplified) looks the following way:
<lightning-datatable
       key-field="Id"
       data={data}
       columns={columns}
       onrowselection={handleSelected}
       selected-rows={preSelectedRows}
       is-loading={tableLoadingState}>
</lightning-datatable>

My purpose: Preselect the current account, because the lwc is placed on a lightning record page for account.

My problem: Nothing happens. No checkbox is active.

One of my first ideas was the key-field. I changed it from "id" (lowercase) to "Id" (uppercase) but that don't solve the problem.

I don't know any other simpler way to test the preselect of rows for datatable than my example above.

Any suggestions?
Best Answer chosen by Christian Schwabe 9
Alain CabonAlain Cabon
I just made a test with a wired part and there is no complication here (no async or setTimeout).

Salesforce: Load data via init handler first then handle programmatic selection.

Just don't use a push.
<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}
                selected-rows={preSelectedRows}
                >
        </lightning-datatable>
        </div>
    </lightning-card>
</template>
 
@track preSelectedRows = [];

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

           let my_ids = [];
           my_ids.push(this.contacts[0].Id);
           my_ids.push(this.contacts[1].Id);
           this.preSelectedRows = my_ids;

           console.log('preSelectedRows:' + this.preSelectedRows);
 
           this.error = undefined;
            
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
        }
    }
 
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;
    }
}

User-added image

So your code should work if you don't use push anymore on the tracked variable (use a new one like above) and if your record Id is included into the read Ids.

Logically, it is always the 18 characters Id for the both values in your case.
 

All Answers

Alain CabonAlain Cabon
Hi Christian,

There is likely a small javascript error before the selection of the rows.

The code below works in the playground.

https://developer.salesforce.com/docs/component-library/tools/playground 
 
<template>
    <lightning-datatable
            data={data}
            columns={columns}
            key-field="id"
            selected-rows={preSelectedRows}
           >
    </lightning-datatable>
</template>
 
import { LightningElement, track } from 'lwc';

const columns = [
     {label: 'Opportunity name', fieldName: 'opportunityName', type: 'text'},
     {label: 'Confidence', fieldName: 'confidence', type: 'percent', cellAttributes:
     { iconName: { fieldName: 'trendIcon' }, iconPosition: 'right' }},
     {label: 'Amount', fieldName: 'amount', type: 'currency', typeAttributes: { currencyCode: 'EUR'}},
     {label: 'Contact Email', fieldName: 'contact', type: 'email'},
     {label: 'Contact Phone', fieldName: 'phone', type: 'phone'},
];

const data = [{
                    id: '0011x00000KOMiJAAX',
                    opportunityName: 'Cloudhub',
                    confidence: 0.2,
                    amount: 25000,
                    contact: 'jrogers@cloudhub.com',
                    phone: '2352235235',
                    trendIcon: 'utility:down'
                },
                {
                    id: '0011x10000KOMiJAAZ',
                    opportunityName: 'Quip',
                    confidence: 0.78,
                    amount: 740000,
                    contact: 'quipy@quip.com',
                    phone: '2352235235',
                    trendIcon: 'utility:up'
                }];

export default class App extends LightningElement {
    @track data = data;
    @track columns = columns;

    @track preSelectedRows = ['0011x00000KOMiJAAX','0011x10000KOMiJAAZ'];

}

 
Alain CabonAlain Cabon
The code is very similar to yours that seems correct at first glance.
But you try perhaps something more complicated with your merge duplicates?
Christian Schwabe 9Christian Schwabe 9
Hi Alain.

thanks for your response and sorry for the delay.

Here it is how it looks like (overall):
 
/* eslint-disable no-console */
import { LightningElement, wire, api, track} from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

import getObjectNameFromRecordId from '@salesforce/apex/MetadataHelper.getObjectNameFromRecordIdPrefix';
import queryRecords from '@salesforce/apex/DuplicateController.queryRecords';

/**
 * @todo: Possibility to select related fields (i.e.: Account.Brand__r.Name): https://wissel.net/blog/2018/08/lightning-datatables-and-relationship-queries.html
 */
export default class MergeDuplicates extends LightningElement {
    /**
     * Following parameter are set by parent lwc.
     */
    @api recordId;
    @api objectApiName = ''; // Contains api-name of object related to current record.
    @api listOfRecords = []; // Contains all records which are potential duplicates.
    @api columnNames; // Specifies which columns are displayed for assertion.
    
    initialRender = true;

    @track data = [];
    @track columns;
    @track tableLoadingState = true;
    @track selectedRows = []; // Contains ids of selected rows
    @track preSelectedRows = [];//'0011x00000KOMiJAAX'
    
    @track currentStep = 'step1';
    @track listOfRecordId = []; // Contains extracted ids for records
    @track firstColumn = []; // Represent 1st column of records in table

    // https://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.reference_wire_adapters_object_info
    @wire(getObjectInfo, { objectApiName: '$objectApiName' })
    wireObjectInfo({error, data}){
        console.log('>>>wireObjectInfo called.');

        if (data) {
            console.log('>>>data: ' + JSON.stringify(data, null, '\t'));
            this.objectInfo = data;
            this.error = undefined;
        } else if (error) {
            console.error('>>>error: ' + JSON.stringify(error, null, '\t'));

            this.error = error;
            this.objectInfo = undefined;
        }
    }

    @wire(queryRecords, { listOfColumnName: '$columnNames', recordId: '$recordId' })
    wiredRecords({ error, data }){
        console.log('>>>wiredRecords in mergeDuplicates.js called.');
        console.log('>>>this.recordId: ' + this.recordId);
        //console.log('>>>this.objectInfo: ' + JSON.stringify(this.objectInfo, null, '\t'));

        if(data){
            console.log('>>>data: ' + JSON.stringify(data, null, '\t'));

            this.data = data;
            this._prepareTableColumns();
            this.preSelectedRows.push(this.recordId); // preselect row(s) programmatically.

        } else if(error){
            console.error('>>>error: ' + JSON.stringify(error, null, '\t'));

            this.data = undefined;
            this.error = error;
        }
    }
    
    @wire(getObjectNameFromRecordId, {recordIdOrPrefix: '$recordId'})
    wiredObjectNameFromRecordId({ error, data }){
        console.log('>>>wiredObjectNameFromRecordId in mergeDuplicates.js called.');
        if(data){
            console.log('>>>data: ' + JSON.stringify(data, null, '\t'));

            this.objectApiName = data.toLowerCase();
            console.log('>>>this.objectApiName: ' + this.objectApiName);
        } else if(error){
            console.error('>>>error: ' + JSON.stringify(error, null, '\t'));

            this.objectApiName = undefined;
            this.error = error;
        }
    }

    handleCloseModal(event) {
        // 1. Prevent default behaviour of anchor click which is to navigate to the href url.
        event.preventDefault();
        
        const oncloseEvent = new CustomEvent('close');
        // 2. Fire the custom event
        this.dispatchEvent(oncloseEvent);
    }

    handleNext(){
        console.log('>>>handleNext in mergeDuplicates.js called.');
        console.log('>>>this.currentStep: ' + this.currentStep);

        switch (this.currentStep) {
            case "step1":
                this.currentStep = "step2";
                break;
            case "step2":
                this.currentStep = "step3";
                break;
            default:
                break;
        }

        console.log('>>>this.currentStep: ' + this.currentStep);
    }

    handleSelected(event) {
        console.log('>>>handleSelected in mergeDuplicates.js called.');

        const selectedRows = event.detail.selectedRows;
        this.selectedRows = []; // Clear all selected values.

        // Display that fieldName of the selected rows
        for (let i = 0; i < selectedRows.length; i++){
            //alert("You selected: " + selectedRows[i].opportunityName);
            this.selectedRows.push(selectedRows[i].Id);
        }
        
        console.log('>>>this.selectedRows: ' + JSON.stringify(this.selectedRows, null, '\t'));
    }

    _prepareTableColumns(){
        console.log('>>>_prepareTableColumns called.');
        this.columns.forEach(column => {
            let columnLabel = ((this.objectInfo) ? this.objectInfo.fields[column.fieldName].label : '');
            column.label = columnLabel;
            console.log('>>>column' + JSON.stringify(column, null, '\t'));
        });
    }

    _prepareTable(){
        console.log('>>>_prepareTable in mergeDuplicates.js called.');
        console.log('>>>this.objectInfo.data: ' + JSON.stringify(this.objectInfo.data, null, '\t'));
        console.log('>>>this.columnNames: ' + JSON.stringify(this.columnNames, null, '\t'));


        //this.columns = JSON.parse(this.columnNames); // Set property from lightning app builder to table column.
        
        /* this.columns.forEach(column => {
            let columnLabel = ((this.objectInfo.data) ? this.objectInfo.data.fields[column.fieldName].label : '');
            column.label = columnLabel;
            console.log('>>>column' + JSON.stringify(column, null, '\t'));
        });
 */

        //console.log('>>>this.preSelectedRows: ' + JSON.stringify(this.preSelectedRows, null, '\t'));
        console.log('>>>this.columns: ' + JSON.stringify(this.columns, null, '\t'));
        console.log('>>>this.columnNames: ' + JSON.stringify(this.columnNames, null, '\t'));
    }

    _getObjectLabelPlural(){
        return ((this.objectInfo) ? this.objectInfo.labelPlural : '');
    }

    get objectLabelPluralToLowerCase(){
        return ((this.objectInfo) ? this.objectInfo.labelPlural.toLowerCase() : '');
    }

    _getNumberOfSelectedRows(){
        return ((this.selectedRows.length) ? this.selectedRows.length : 0);
    }

    /**
     * Indicates if "Next"-button is disabled or not.
     */    
    get nextButtonDisabled(){
        console.log('>>>nextButtonDisabled in mergeDuplicates.js called.');
        console.log('>>>this._getNumberOfSelectedRows(): ' + this._getNumberOfSelectedRows());

        return ((this._getNumberOfSelectedRows() > 3 || this._getNumberOfSelectedRows()) <= 1 ? true : false);
    }

    get isFirstStep(){
        return ((this.currentStep === "step1") ? true : false);
    }

    get isSecondStep(){
        return ((this.currentStep === "step2") ? true : false);
    }

    get nameOfObjectAndNumberOfRecordsInTable(){
        let objectLabelPlural = this._getObjectLabelPlural();
        let numberOfRecordsInTable = ((this.data) ? this.data.length : 0);
        
        return objectLabelPlural + ' (' + numberOfRecordsInTable + ')';
    }

    /**
    * https://developer.salesforce.com/docs/component-library/documentation/lwc/create_lifecycle_hooks_dom.html
    * The connectedCallback() lifecycle hook fires when a component is inserted into the DOM.
    */
    connectedCallback() {
        console.log('>>>connectedCallback in mergeDuplicates.js called.');

        this.columns = JSON.parse(this.columnNames); // Set property from lightning app builder to table column.
        this.tableLoadingState = false;
    }

    /**
     * https://developer.salesforce.com/docs/component-library/documentation/lwc/create_lifecycle_hooks_rendered.html
     * Use it to perform logic after a component has finished the rendering phase.
     */
    /* renderedCallback(){
        console.log('>>>renderedCallback in mergeDuplicates.js called.');
        //console.log('>>>this.objectInfo: ' + JSON.stringify(this.objectInfo, null, '\t'));

        if(this.initialRender){// Do it only once.
            this.initialRender = false;
        }


        console.log('>>>this.listOfRecords: ' + JSON.stringify(this.listOfRecords, null, '\t'));
    } */
}

Any ideas for this behaviour?
Alain CabonAlain Cabon
Hi Christian,

From the async connectedCallback (equivalent of the init in Aura )  that still works.

It seems that the push doesn't work but a simpler assignment like below works.
LWC is very capricious and I struggle myself a lot in order to find the right code ( push and sort must be banned for the tracked variables).
 
this.data = data;
this.preSelectedRows = [ this.recordId ]; // preselect row(s) programmatically.

Playground:
import { LightningElement, track } from 'lwc';

const columns = [
     {label: 'Opportunity name', fieldName: 'opportunityName', type: 'text'},
     {label: 'Confidence', fieldName: 'confidence', type: 'percent', cellAttributes:
     { iconName: { fieldName: 'trendIcon' }, iconPosition: 'right' }},
     {label: 'Amount', fieldName: 'amount', type: 'currency', typeAttributes: { currencyCode: 'EUR'}},
     {label: 'Contact Email', fieldName: 'contact', type: 'email'},
     {label: 'Contact Phone', fieldName: 'phone', type: 'phone'},
];

const data = [{
                    id: '0011x00000KOMiJAAX',
                    opportunityName: 'Cloudhub',
                    confidence: 0.2,
                    amount: 25000,
                    contact: 'jrogers@cloudhub.com',
                    phone: '2352235235',
                    trendIcon: 'utility:down'
                },
                {
                    id: '0011x10000KOMiJAAZ',
                    opportunityName: 'Quip',
                    confidence: 0.78,
                    amount: 740000,
                    contact: 'quipy@quip.com',
                    phone: '2352235235',
                    trendIcon: 'utility:up'
                }];

export default class App extends LightningElement {
    @track data = data;
    @track columns = columns;

    @track preSelectedRows; 

    async connectedCallback() {
      let rowId = '0011x00000KOMiJAAX';
      this.preSelectedRows = [rowId];
   }
}

I will test with a "wire" tonight (not sure that will work for you yet).
 
Christian Schwabe 9Christian Schwabe 9
Hi Alaine,

thanks for your response.

I try to adopt it in this playground (https://developer.salesforce.com/docs/component-library/tools/playground/SW3lzOCW3/2/edit) and analyze the difference. I can't get it to work. The only(!) difference is that I work with "this.recordId" to add recordId dynamically. Could this be a problem?
There is no usecase to hardcode ids.


Kind regards,
Chris
Alain CabonAlain Cabon
I just made a test with a wired part and there is no complication here (no async or setTimeout).

Salesforce: Load data via init handler first then handle programmatic selection.

Just don't use a push.
<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}
                selected-rows={preSelectedRows}
                >
        </lightning-datatable>
        </div>
    </lightning-card>
</template>
 
@track preSelectedRows = [];

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

           let my_ids = [];
           my_ids.push(this.contacts[0].Id);
           my_ids.push(this.contacts[1].Id);
           this.preSelectedRows = my_ids;

           console.log('preSelectedRows:' + this.preSelectedRows);
 
           this.error = undefined;
            
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
        }
    }
 
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;
    }
}

User-added image

So your code should work if you don't use push anymore on the tracked variable (use a new one like above) and if your record Id is included into the read Ids.

Logically, it is always the 18 characters Id for the both values in your case.
 
This was selected as the best answer
Christian Schwabe 9Christian Schwabe 9
Hi Alain,

thank you so much for your support. I've done it now and it works now.
It looks like the following:
@wire(queryRecords, { listOfColumnName: '$columns', recordId: '$recordId' })
    wiredRecords({ error, data }){
        if(data){
            this.data = data;
            this.preSelectedRows = [this.recordId];// Programmatically preselect current record.
        } else if(error){
            this.data = undefined;
            this.error = error;
        }
    }

I think I also found the explanation for this behaviour: https://salesforce.stackexchange.com/questions/252996/array-push-wont-update-lightning-web-component-view
Generally the problem is the observation for the pointer and not the object (array) itself.

Difficult to get behind this behavior if it is not so obviously described. If you get to know this behaviour and know about it, you can explain it well. But to get to this point is quite difficult.

Kind regards,
Chris

 
Alain CabonAlain Cabon
Ok, Good.

Interesting and this explanation should exist in the documentation of Salesforce (... somewhere).

Simply, if we just use direct assignation ( = ) without "push" or "sort" on the tracked variable, that should always works for the refresh of LWC.
With Aura Ligthning, we use always explicit updates of the component with: cmp.set('v.field', value);  and there are not these subtle distinctions like in LWC.

This problem is disturbed at the beginning because the trace logs show nothing wrong like in your case.
I will see probably many times the same question in the coming months in this forum and this precise explanation will be very useful.

Kind regards,
Alain
Alain CabonAlain Cabon
Simply, if we just use direct assignation ( = ) without "push" or "sort" on the tracked variable, that should always work for the refresh of LWC.
This problem is disturbing at the beginning because the trace logs show nothing wrong like in your case.

Thanks again for the link. It is sometimes very time consuming when we search the best explanation.

(sorry for my broken english, if you understood my answer nevertheless that is the most important)
Raghavendra ARaghavendra A
Hi All,

I hope others are still able to get updates to this post. 

For some reason this does not work for me at all. Even after following the pattern  this.preSelectedRows = [this.preSelectedRowId];

The only difference is that this we have used this as a generic component and being invoked by another parent component. preSelectedRowId is a @api public attribute and I can see both values (preSelectedRows & preSelectedRowId) match on the console. preSelectedRows is a tracked variable. 

Any help is highly appreciated!

Raghu
Venu9999Venu9999
If we hard code selected records while property creation itself, then rows will not be selected. We need to assign selected values programatically or we can assign values after data loaded. 
if(result.data) {
   this.oppList=result.data.;
   this.errorList=undefined;
    this.preSelectedRows =["0060K00000baGN5QAM","0060K00000baGN9QAM","0060K00000baGNEQA2"];
}
How to pre popuate sected records  Check exampe here (https://www.salesforcepoint.com/2020/07/preselected-checkbox-data-table-Lwc.html