wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Re-Usable Dynamic Custom Lookup LWC edition


Over at sfdcmonkey there's a nice AURA component that allows for dynamic lookup of a given object. Super nice and useful. I wondered what it would take to be rebuild in LWC

Dynamic Lookup

Same, Same but different

I want to achieve the same functionality, but would accept subtle differences. This is what I got:

  • The original component shows the object icon on the left. My version shows the search symbol that comes out of the box with lightning-input
  • I use 3 components instead of two. Input fields that trigger network calls are fairly common and it makes sense to debounce the input. So I created c-ux-debounced-input that signals entered data only after a period of 300ms
  • The component dispatches an event when a result has been selected or cleared, so it can be used inside other components
  • For now: it can be directly put on a lightning page in page builder and configured there. Useful for demos and test
  • When you clear the selected object, the result list opens up again, so no second network call is made until you change the input value

The code

The template is slightly lighter

<template>
    <div class={resultClass} data-select="single">
        <div class="slds-form-element__control">
            <!-- sequence completed record selected -->
            <div if:true={selectedRecord}>
                <label>{label}</label>
                <div class="slds-pill-container">
                    <lightning-pill
                        class="pillSize"
                        label={selectedRecord.Name}
                        onremove={handlePillRemove}>
                        <lightning-icon
                            icon-name={iconName}
                            variant="plain"
                            alternative-text={selectedRecord.Name}>
                        </lightning-icon>
                    </lightning-pill>
                </div>
            </div>
            <!-- Input for search term -->
            <c-ux-debounced-input
                label={label}
                onchange={handleSearchTerm}
                if:false={selectedRecord}
                value={lastSearchValue}>
            </c-ux-debounced-input>
        </div>
        <ul
            style="min-height:40px;margin-top:0px !important"
            class="slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid slds-lookup__menu slds"
            role="listbox">
            <lightning-spinner
                variant="brand"
                size="small"
                if:true={showSpinner}
            ></lightning-spinner>
            <center>{message}</center>
            <template if:true={results} for:each={results} for:item="singleRec">
                <c-ux-quick-lookup-result
                    icon-name={iconName}
                    record={singleRec}
                    onselection={handleRecordSelect}
                    key={singleRec.Id}
                ></c-ux-quick-lookup-result>
            </template>
        </ul>
    </div>
</template>

The CSS is more or less the same

.pillSize {
    width: 100%;
}

The JavaScript is proper ES6. It already contains some code for my "Next Stop"

import { LightningElement, api, track } from 'lwc';
import fetchLookUpValues from '@salesforce/apex/CustomLookUpController.fetchLookUpValues';
import fetchExtendedLookUpValues from '@salesforce/apex/CustomLookUpController.fetchExtendedLookUpValues';

export default class UxQuickLookup extends LightningElement {
    @api objectApiName;
    @api iconName = 'standard:account';
    @api label = 'Lookup';
    @api fields = null;

    @track resultClass;
    @track selectedRecord = null;
    @track results = null;
    @track message = null;
    @track showSpinner = false;
    @track lastSearchValue;

    constructor() {
        super();
        this.switchResult(false);
    }

    handleSearchTerm(event) {
        let searchValue = event.detail;
        if (searchValue) {
            this.switchResult(true);
            this.message = 'searching...';
            this.showSpinner = true;
            let searchParams = {
                searchKeyWord: searchValue,
                objectName: this.objectApiName
            };
            if (this.fields) {
                this.addFieldsToParam(searchParams);
                fetchExtendedLookUpValues(searchParams)
                    .then(result => this.setResult(result))
                    .catch(error => this.handleError(error));
            } else {
                fetchLookUpValues(searchParams)
                    .then(result => this.setResult(result))
                    .catch(error => this.handleError(error));
            }
        } else {
            this.switchResult(false);
            this.message = null;
            this.showSpinner = false;
            this.results = null;
        }
        this.lastSearchValue = searchValue;
    }

    /* Ensure we always have Name and Id in the query */
    addFieldsToParam(searchParam) {
        let allFields = this.fields.split(',');
        allFields.push('Id');
        allFields.push('Name');
        let cleanFields = this.dedupeArray(allFields).join(',');
        searchParam.fieldsToQuery = cleanFields;
    }

    dedupeArray(incoming) {
        var uniqEs6 = arrArg => {
            return arrArg.filter((elem, pos, arr) => {
                return arr.indexOf(elem) === pos;
            });
        };
        return uniqEs6(incoming);
    }

    setResult(newValues) {
        this.showSpinner = false;
        if (newValues && newValues.length > 0) {
            this.message = null;
            this.results = newValues;
        } else {
            this.message = 'no results found';
        }
    }

    /* Shows and hides the result area */
    switchResult(on) {
        this.resultClass = on
            ? 'slds-form-element slds-lookup slds-is-open'
            : 'slds-form-element slds-lookup slds-is-close';
    }

    handlePillRemove() {
        this.selectedRecord = null;
        let payload = {
            detail: {
                canceled: true,
                recordId: null
            }
        };
        let selected = new CustomEvent('selection', payload);
        this.dispatchEvent(selected);
        // Restore last results
        this.switchResult(this.lastSearchValue && this.results);
    }

    handleError(error) {
        this.showSpinner = false;
        this.message = "Sorry didn't work!";
        let errorDispatch = new CustomEvent('failure', { detail: error });
        this.dispatchEvent(errorDispatch);
    }

    handleRecordSelect(event) {
        let record = event.detail;
        this.selectedRecord = record;
        let selectionDispatch = new CustomEvent('recordselected', {
            detail: record
        });
        this.dispatchEvent(selectionDispatch);
        this.switchResult(false);
    }
}

What you don't see here are c-ux-quick-lookup-result and c-ux-debounced-input. To check those, head over to the repository, have a look, take a clone and deploy it with sfdx force:source:deploy -p experiments/experiment5 into a sandbox. Add the component to a lightning page and have a look.

Next Stop

When selecting object, the name might not be enough to pick the right value. So I'm looking into a way to optional display more field values. That's an interesting challenge for visualization.

As usual: YMMV


Posted by on 27 March 2019 | Comments (2) | categories: Lightning Salesforce Software

Comments

  1. posted by Nagarjun on Wednesday 10 July 2019 AD:

    we can't able find the rest of the components c-ux-quick-lookup-result and c-ux-debounced-input in your git repo.
    Please share the code.


  2. posted by Brooks on Friday 25 October 2019 AD:

    First of all, thank you so much for writing this. Saved me and presumably many others from having to work all of this out on our own. I noticed that the record list in your example flickers a bit and doesn't show up all the time. I figured out that by adding the following code, it sticks around properly, so I thought I would share (one very small change to customLookup.js):

    handleOnchange(event) {
        // Collect search key from event
        const searchKey = event.detail.value;
    
        // Return case - (MODIFICATION)
        if (searchKey === null || searchKey === undefined) {
            return;
        }
        else if (searchKey.length === 0) {
            this.records = undefined;
            return;
        } ...