Using custom LWC components in lightning datatable

Using this article, you will be able to use custom LWC lightning component in lightning-datatable. PFB screenshot:

Here I created custom component to show in column UPLOAD

For doing this, you can follow 3 steps as below:

Step 1 : create custom component as follows.

pocCustomComp.html :

<template>
    <div>
        Id: {recordId}
    </div>
    <lightning-file-upload label="Upload"
                           name="fileUploader"
                           accept={acceptedFormats}
                           record-id={recordId}
                           onuploadfinished={handleUploadFinished}>
    </lightning-file-upload>
</template>
export default class PocCustomComp extends LightningElement {
    @api recordId;
    @api acceptedFormats;
    handleUploadFinished() {
        this.dispatchEvent(new CustomEvent('uploadfinished', {
            composed: true,
            bubbles: true,
            cancelable: true,
            detail: {
                data: { name: 'some data', recordId: this.recordId }
            }
        }));
        this.dispatchEvent(new ShowToastEvent({
            title: 'Completed',
            message: 'File has been uploaded',
        }));
    }
}

Step 2 : Create a new component which extends datatable

pocLightningDatatable.js:

import LightningDatatable from 'lightning/datatable';
import pocFileUpload from './pocFileUpload.html';

export default class PocLightningDatatable extends LightningDatatable {
    static customTypes = {
        fileUpload: {
            template: pocFileUpload,
            typeAttributes: ['acceptedFormats'],
        }
    };
}

Here we are defining custom data types – fileUpload. Also do you notice that we are importing pocFileUpload template? You need to create pocFileUpload.html in the same folder as pocLightningDatatable

pocFileUpload.html – you have to use the custom component that we created before.

<!-- pocFileUpload.html -->
<template>
    <c-poc-custom-comp record-id={value}
                       accepted-formats={typeAttributes.acceptedFormats}>
    </c-poc-custom-comp>
</template>

Observe the usage of value here, it is automatically passed as the value for the fieldName we define in columns.

Step 3 : We can use the component (that extended datatable) where-ever we want

<template>
	<c-poc-lightning-datatable key-field="id"
				data={data}
				columns={columns}
				onuploadfinished={handleUploadFinished}
				hide-checkbox-column>
	</c-poc-lightning-datatable>
</template>

And the javascript which uses custom datatype:

@track data = [];
	connectedCallback() {
		this.columns = [
			{ label: 'Name', fieldName: 'Name', typeAttributes: { acceptedFormats: '.jpg,.jpeg,.pdf,.png' } },
			{ label: 'Account Number', fieldName: 'AccountNumber' },
			{ label: 'Upload', type: 'fileUpload', fieldName: 'Id' }
		];
		findAccounts().then(res => { this.data = res; }).catch(err => console.error(err));
	}
	handleUploadFinished(event) {
		event.stopPropagation();
		console.log('data => ', JSON.stringify(event.detail.data));
	}

Note: pocLightningDatatable.html will not have anything in it.

LWC – Access Templates and Methods of child components

Normally closed shadow DOM/root will not allow the access of child component’s DOM. Consider below example:

<main-component>
    <parent>
        <child>
            <iteration>
                <grand-child>
            </iteration>

How can you change styles/add classes OR invoke methods directly in grand-child from main-component? Ofcourse you can have public API methods at each level but this will make it very complicated with many children. So, is there a more simpler and elegant way?

Yes! You can have simple generic registration for all elements in main-component. Then, which ever child component (in any hierarchy) has to be accessed, fire a custom registration event with appropriate details.

Grand child HTML:

<template>
    <div id="main" class="main">
        <div class="my-class">
            This is grand-child component body
        </div>
        <div class="dynamic"></div>
    </div>
</template>

You need a main class which wraps whole content.

Grand child JS:

export default class GrandChild extends LightningElement {
    renderedCallback() {
        if (!this.guid) {
            this.guid = this.template.querySelector('.main').getAttribute('id');
            this.dispatchEvent(
                new CustomEvent('itemregister', {
                    bubbles: true,
                    composed: true,
                    detail: {
                        callbacks: {
                            dynamicData: this.dynamicData
                        },
                        template: this.template,
                        guid: this.guid,
                        name: 'c-grand-child'
                    }
                })
            );
        }
    }
    dynamicData = (data) => {
        this.template.querySelector('.dynamic').innerText = data;
    }
}

You need to dispatch custom event with name, guid, template and callbacks. This cannot be done in connectedCallback because in its scope elements will not be created and so elements will be undefined. In renderedCallback, we will be dispatching event only once with help of checking guid.

  1. Template can be used to modify styles or add classes or any similar functionality.
  2. Callbacks will have the exposed functions references
  3. Name and guid are used for registartion

Child HTML

<template for:each={iteration} for:item='it'> 
	<div key={it.mid}>
		<c-grand-child></c-grand-child>
	</div>
</template>

Child JS:

export default class Child extends LightningElement {
	iteration = [{ mid: '1' }, { mid: '2' }, { mid: '3' }];
}

Parent HTML

<c-child></c-child>

Main component HTML:

<lightning-button label="Add Styles" onclick={addStyles}></lightning-button>
<lightning-button label="Add Data" onclick={addData}></lightning-button>

<div onitemregister={registerItem}>
       <c-parent></c-parent>
</div>

Main component JS:

    privateChildren = {};
	registerItem(event) {
		event.stopPropagation();
		const item = event.detail;

		// create key for each child against its name
		if (!this.privateChildren.hasOwnProperty(item.name)) this.privateChildren[item.name] = {};

		// store each item against its guid
		this.privateChildren[item.name][item.guid] = item;
	}
	addStyles() {
		Object.values(this.privateChildren['c-grand-child']).forEach((element) => {
			element.template.querySelector('.my-class').style.color = 'white';
			element.template.querySelector('.my-class').style.backgroundColor = 'blue';
		});
	}
	addData() {
		Object.values(this.privateChildren['c-grand-child']).forEach((element, index) => {
			element.callbacks.dynamicData('Changing dynamic data => ' + index);
		});
	}

Here, you are using registerItem generic method to register all needed child elements with key as its names. Later you can use the names to access the callbacks and template.

Screenshot before:

Screenshot After:

Dynamic and Reactive Javascript Class Properties in LWC (track, wire and $)

Consider a scenario: When text is entered in input, it should search accounts with that search text.

poc.html

<template>
	<lightning-input onchange={doSearch} value={accName}></lightning-input>

	<template if:true={accs.data}>
		<template for:each={accs.data} for:item='pd'>
			<div key={pd.Id} class="slds-box">
				<lightning-formatted-text value={pd.Name}></lightning-formatted-text>
			</div>
		</template>
	</template>
	<template if:true={accs.error}>ERROR => {accs.error}</template>

</template>

Below is the Javascript of poc component:

poc.js

import { LightningElement, track, wire } from 'lwc';
import getAccounts from '@salesforce/apex/poc.getAccounts';

export default class Poc extends LightningElement {
	@track accName;
	@wire(getAccounts, { accName: '$accName' })
	accs;

	getAccName(event) {
		this.accName = event.detail.value;
	}
	renderedCallback() {
		console.log("RENDERED POC");
	}
}

As you observe, we are logging “RENDERED POC” everytime it rerenders.

poc.class

@AuraEnabled(cacheable=true)
    public static List<Account> getAccounts(String accName){
        return [SELECT Id, Name FROM Account WHERE Name LIKE :('%'+accName+'%')];
    }

What will happen when you enter something?

accName will get updated in getAccName method AND component immediately rerenders (as accName is tracked property) and this can be observed in log. ‘$accName’ is dynamic and reactive property. Change in accName will trigger the wire function which is dependent on ‘$accName’ and wire will modify the value of accounts which is tracked property. Thus the component rerenders again! This may lead to performance degradation.

So, what can we do?

  1. Do not track dynamic properties.
  2. Remove the references of dynamic properties in html and use separate tracked properties.

Remove accName from input. In any case, that value doesnt get updated directly. We need to update accName in change handler. If we want to prepolute input, we should use separate tracked property.

<lightning-input onchange={getAccName}></lightning-input>

In javascript, do not track accName.

export default class Poc extends LightningElement {
	@wire(getAccounts, { accName: '$accName' })
	accs;

	getAccName(event) {
		this.accName = event.detail.value;
	}
	renderedCallback() {
		console.log("RENDERED POC");
	}
}

Now load the page and start search again. You will observe that component will rerender only once instead of 2 times for each search.

Reason: As accName is referred dynamically in wire, a change in accName will automatically trigger the wire function and thus it will update accounts.

LWC Selectors/Identification of elements

From the documentation of LWC, it is clear that we can use only querySelector and querySelectorAll for getting/selecting element(s) and that we cannot use ID selectors because they are modified at runtime (when rendered).

Now consider the below code:

<div class="first-class">
	<lightning-input class="second-class" ></lightning-input>
	<lightning-input class="second-class" ></lightning-input>
	<lightning-input class="second-class" ></lightning-input>
</div>

How will you set the value of exactly 3rd input dynamically? (Remember you cannot use Id).

Below is the way you can do it.

let firstClass = this.template.querySelector(".first-class");
let secondClasses = firstClass.querySelectorAll(".second-class");
secondClasses[2].value = 'Some Value';

Well, above logic seems confusing or more than necessary code lines? Can we make it better? Yes of-course!

We can use some other attribute(s) to select an element we want. As a safe method we can add data- attributes.

<div class="first-class">
	<lightning-input data-my-id="in1" class="second-class"></lightning-input>
	<lightning-input data-my-id="in2" class="second-class"></lightning-input>
	<lightning-input data-my-id="in3" class="second-class"></lightning-input>
</div>

Now to select the third input and set the value, use below:

this.template.querySelector("lightning-input[data-my-id=in3]").value = "Some Value";

Custom datatable in LWC (Fixed-header,Resizable,Scroll etc)

Here you will learn to build custom data table with:

  1. Fixed header
  2. Re-sizable columns
  3. Horizontal scroll
  4. Double-click anywhere on table to resize to initial widths

HTML

<template>

	<div id="containerDiv" onmousemove={handlemousemove} onmouseup={handlemouseup}
		 ondblclick={handledblclickresizable}
		 class="slds-table_header-fixed_container slds-border_right slds-border_left tableScroll"
		 onscroll={tableOuterDivScrolled}>
		<div id="tableViewInnerDiv" onscroll={tableScrolled} class="slds-scrollable_y tableViewInnerDiv">

			<table
				   class="slds-table slds-table_bordered slds-table_header-fixed slds-table_resizable-cols slds-table_fixed-layout">
				<thead>
					<tr>
						<th class="slds-is-resizable dv-dynamic-width" scope="col" style={fixedWidth} title="Column 1">
							<div class="slds-cell-fixed" style={fixedWidth}>
								<a class="slds-th__action slds-text-link--reset ">
									<span class="slds-truncate">Column 1</span>
								</a>
								<div class="slds-resizable">
									<span class="slds-resizable__handle" onmousedown={handlemousedown}>
										<span class="slds-resizable__divider"></span>
									</span>
								</div>
							</div>
						</th>
						<th class="slds-is-resizable dv-dynamic-width" scope="col" style={fixedWidth} title="Column 2">
							<div class="slds-cell-fixed" style={fixedWidth}>
								<a class="slds-th__action slds-text-link--reset ">
									<span class="slds-truncate">Column 2</span>
								</a>
								<div class="slds-resizable">
									<span class="slds-resizable__handle" onmousedown={handlemousedown}>
										<span class="slds-resizable__divider"></span>
									</span>
								</div>
							</div>
						</th>
						<th class="slds-is-resizable dv-dynamic-width" scope="col" style={fixedWidth} title="Column 3">
							<div class="slds-cell-fixed" style={fixedWidth}>
								<a class="slds-th__action slds-text-link--reset ">
									<span class="slds-truncate">Column 3</span>
								</a>
								<div class="slds-resizable">
									<span class="slds-resizable__handle" onmousedown={handlemousedown}>
										<span class="slds-resizable__divider"></span>
									</span>
								</div>
							</div>
						</th>
						<th scope="col">
							<div class="slds-cell-fixed">
								 
							</div>
						</th>
					</tr>
				</thead>
				<tbody>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
					<tr>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Made neat an on be gave show
								snug tore neat an on be gave
							</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>Piqued favour</div>
						</td>
						<td>
							<div class="slds-truncate dv-dynamic-width" style={fixedWidth}>behaviour</div>
						</td>
						<td></td>
					</tr>
				</tbody>
			</table>
		</div>
	</div>
	
</template>

JS

import {
    LightningElement, track
} from 'lwc';

export default class DemoDataViewFieldsConfig extends LightningElement {

    fixedWidth = "width:15rem;";

    //FOR HANDLING THE HORIZONTAL SCROLL OF TABLE MANUALLY
    tableOuterDivScrolled(event) {
        this._tableViewInnerDiv = this.template.querySelector(".tableViewInnerDiv");
        if (this._tableViewInnerDiv) {
            if (!this._tableViewInnerDivOffsetWidth || this._tableViewInnerDivOffsetWidth === 0) {
                this._tableViewInnerDivOffsetWidth = this._tableViewInnerDiv.offsetWidth;
            }
            this._tableViewInnerDiv.style = 'width:' + (event.currentTarget.scrollLeft + this._tableViewInnerDivOffsetWidth) + "px;" + this.tableBodyStyle;
        }
        this.tableScrolled(event);
    }

    tableScrolled(event) {
        if (this.enableInfiniteScrolling) {
            if ((event.target.scrollTop + event.target.offsetHeight) >= event.target.scrollHeight) {
                this.dispatchEvent(new CustomEvent('showmorerecords', {
                    bubbles: true
                }));
            }
        }
        if (this.enableBatchLoading) {
            if ((event.target.scrollTop + event.target.offsetHeight) >= event.target.scrollHeight) {
                this.dispatchEvent(new CustomEvent('shownextbatch', {
                    bubbles: true
                }));
            }
        }
    }

    //#region ***************** RESIZABLE COLUMNS *************************************/
    handlemouseup(e) {
        this._tableThColumn = undefined;
        this._tableThInnerDiv = undefined;
        this._pageX = undefined;
        this._tableThWidth = undefined;
    }

    handlemousedown(e) {
        if (!this._initWidths) {
            this._initWidths = [];
            let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
            tableThs.forEach(th => {
                this._initWidths.push(th.style.width);
            });
        }

        this._tableThColumn = e.target.parentElement;
        this._tableThInnerDiv = e.target.parentElement;
        while (this._tableThColumn.tagName !== "TH") {
            this._tableThColumn = this._tableThColumn.parentNode;
        }
        while (!this._tableThInnerDiv.className.includes("slds-cell-fixed")) {
            this._tableThInnerDiv = this._tableThInnerDiv.parentNode;
        }
        console.log("handlemousedown this._tableThColumn.tagName => ", this._tableThColumn.tagName);
        this._pageX = e.pageX;

        this._padding = this.paddingDiff(this._tableThColumn);

        this._tableThWidth = this._tableThColumn.offsetWidth - this._padding;
        console.log("handlemousedown this._tableThColumn.tagName => ", this._tableThColumn.tagName);
    }

    handlemousemove(e) {
        console.log("mousemove this._tableThColumn => ", this._tableThColumn);
        if (this._tableThColumn && this._tableThColumn.tagName === "TH") {
            this._diffX = e.pageX - this._pageX;

            this.template.querySelector("table").style.width = (this.template.querySelector("table") - (this._diffX)) + 'px';

            this._tableThColumn.style.width = (this._tableThWidth + this._diffX) + 'px';
            this._tableThInnerDiv.style.width = this._tableThColumn.style.width;

            let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
            let tableBodyRows = this.template.querySelectorAll("table tbody tr");
            let tableBodyTds = this.template.querySelectorAll("table tbody .dv-dynamic-width");
            tableBodyRows.forEach(row => {
                let rowTds = row.querySelectorAll(".dv-dynamic-width");
                rowTds.forEach((td, ind) => {
                    rowTds[ind].style.width = tableThs[ind].style.width;
                });
            });
        }
    }

    handledblclickresizable() {
        let tableThs = this.template.querySelectorAll("table thead .dv-dynamic-width");
        let tableBodyRows = this.template.querySelectorAll("table tbody tr");
        tableThs.forEach((th, ind) => {
            th.style.width = this._initWidths[ind];
            th.querySelector(".slds-cell-fixed").style.width = this._initWidths[ind];
        });
        tableBodyRows.forEach(row => {
            let rowTds = row.querySelectorAll(".dv-dynamic-width");
            rowTds.forEach((td, ind) => {
                rowTds[ind].style.width = this._initWidths[ind];
            });
        });
    }

    paddingDiff(col) {

        if (this.getStyleVal(col, 'box-sizing') === 'border-box') {
            return 0;
        }

        this._padLeft = this.getStyleVal(col, 'padding-left');
        this._padRight = this.getStyleVal(col, 'padding-right');
        return (parseInt(this._padLeft, 10) + parseInt(this._padRight, 10));

    }

    getStyleVal(elm, css) {
        return (window.getComputedStyle(elm, null).getPropertyValue(css))
    }

}

CSS

.tableScroll {
    overflow: auto;
    overflow-y: hidden;
    height: 10rem;
}
Design a site like this with WordPress.com
Get started