Friday, May 1, 2020

Lightning Web Components: Custom Styling in Inner Web Elements

How to do custom styling in LWC?

Let say a developer needs to set height and width of lightning-textarea. However facing trouble in setting it with the conventional manner, like below:

customStylingCompLWC.html

<template>
    <lightning-card title="Custom Styling Component LWC">
        <lightning-layout>
            <lightning-layout-item>
               <lightning-textarea name="input2" label="Textarea field with a predefined value" value="initial value" class="customTextarea"></lightning-textarea>  
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>

customStylingCompLWC.css

.customTextarea .slds-form-element{
    background-color: red;
    min-height: 150px;
    min-width: 300px;
    display: inline-block;
}

Preview:


Now the question is, why the custom styling is not working as expected.

If you look at the DOM, its clearly shown that "customTextarea" class is added to wrapper component. So even if you increase the height, the height of inner element wont be changed. It will be changed only on wrapper component.  
Also if you perform something like:

.customTextarea .slds-textarea{
    height: 150px;
}

Unlike Aura Component, this wont work in LWC. Because inner element is closed in Shadow DOM.

Now how to achieve custom styling in LWC? 

Custom Styling is still achievable. However there are certain work around we have to perform. So if you notice the DOM, you will understand that the style is always loaded at the time of page load. And we know it that if we load our custom style at the time of loading only then it can work.
So here you are:

Steps:
1. Load your custom css file in Static Resource.
2. import your file in your js controller.
3. load your style in connected callback().

Example:

customStylingComponentLWC.js

import { LightningElement } from 'lwc';
import { loadStyle } from 'lightning/platformResourceLoader';
import CSS_FILE from '@salesforce/resourceUrl/customStylingCSS';

export default class CustomStylingComponentLWC extends LightningElement {
    connectedCallback() {
        loadStyle(this, CSS_FILE)
        .then(() => {});
    }
}

customStylingCSS (static resource)

.slds-textarea{
    background-color: red;
    min-height: 150px;
    min-width: 300px;
    display: inline-block;
}

Preview:


** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**


References:


Sunday, April 26, 2020

Lightning Web Components: Custom Reusable Lookup Field

How to create custom reusable lookup field in LWC? Lets figure out.

Component Bundles:
  1. lookupFieldComponentLWC (Parent Comp)
  2. lookupResultComponentLWC (Child Comp)
  3. ObjectManager (Apex Class)

** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**

References:

Demo:



Code:


lookupFieldComponentLWC.html

<template>
    <lightning-card title="Custom Reusable Lookup Field">
        <lightning-layout>
            <lightning-layout-item>
                <template if:false={selectedValue}>
                    <lightning-input type="text" placeholder="Input text.." onchange={inputTextHandler}></lightning-input>
                    <template if:true={searchKey}>
                        <template if:true={allRecords}>
                            <template for:each={allRecords} for:item= "record">
                                <c-lookup-result-component-l-w-c key={record.Id} record={record} icon-name={iconName} onselect={handleSelect}></c-lookup-result-component-l-w-c>
                            </template>
                        </template>
                    </template>
                </template>
                <template if:true={selectedValue}>
                    <div class="slds-combobox__form-element slds-input-has-icon 
                                slds-input-has-icon_left-right" role="none">
                        <span class="slds-icon_container
                                    slds-icon-standard-account 
                                    slds-combobox__input-entity-icon" title="Account">
                            <lightning-icon icon-name={iconName} ></lightning-icon>
                        </span>
                        <input class="slds-input slds-combobox__input
                            slds-combobox__input-value" 
                            id="combobox-id-5" aria-controls="listbox-id-5" 
                            autocomplete="off" role="textbox" type="text" 
                            placeholder="Select an Option" readonly=""
                            value={selectedValue.Name}
                            disabled
                            />
                        <button class="sicon_container slds-button slds-button_icon 
                                    slds-input__icon slds-input__icon_right" 
                                title="Remove selected option"
                                onclick={handleRemove}>
                            <lightning-icon icon-name="utility:close" size="small">

                            </lightning-icon>
                            <span class="slds-assistive-text">Remove selected option</span>
                        </button>
                    </div>
                </template>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>

--------------------------------------------------------------------------------------------------------------------------

lookupFieldComponentLWC.js

import { LightningElement, api, track } from 'lwc';
import getAllRecords from '@salesforce/apex/ObjectManager.getRecords';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class LookupFieldComponentLWC extends LightningElement {

    @track searchKey;
    @track allRecords = [];
    @track recordId;
    @track selectedValue;
    @api objectName = 'Account'; //to reuse for other object change the object here
    @api iconName = 'standard:account'; //use the respective object icon
    @api searchField= 'Name'; // use the field for search

    inputTextHandler(event){
        this.searchKey= event.detail.value;
        console.log('searchKey>>'+this.searchKey);
        if(this.searchKey){
            this.getRecords();
        }
    }

    getRecords(){
        getAllRecords({searchKey : this.searchKey, objectName : this.objectName, searchField : this.searchField}).then((allRecords) =>{
            this.allRecords = allRecords;
            console.log('allRecords>>>'+this.allRecords[0]);
            for(let i=0;i<this.allRecords.length;i++){
                const rec = this.allRecords[i];
                this.allRecords[i].Name= rec[this.searchField];
            }
        }).catch((error)=>{
            this.showToast('ERROR',error.body.message, 'error');
        })
    }

    handleSelect(event){
        this.recordId = event.detail; //selected recordId
        this.selectedValue = this.allRecords.find(record=> record.Id === this.recordId); //selected record
        console.log('this.recordId>>>'+this.recordId);
        
    }

    handleRemove(){
        this.selectedValue = undefined;
        this.allRecords = undefined;
    }

    showToast(title, message, variant) {
        const evt = new ShowToastEvent({
            title: title,
            message: message,
            variant: variant,
        });
        this.dispatchEvent(evt);
    }
}

--------------------------------------------------------------------------------------------------------------------------

lookupResultComponentLWC.html

<template>
    <div >
        <div class="slds-grid slds-wrap 
                        slds-dropdown_length-with-icon-7 
                        slds-dropdown_fluid
                        slds-p-left_small">
                <div class="slds-col slds-size_4-of-4 ">
                    <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                        <li role="presentation" class="slds-listbox__item">
                            <div class="slds-media slds-listbox__option 
                                                        slds-listbox__option_entity 
                                                        slds-listbox__option_has-meta" 
                                                        role="option"
                                onclick={handleSelect}>
                                <span class="slds-media__figure slds-listbox__option-icon">
                                    <lightning-icon icon-name={iconName} size="small"></lightning-icon>
                                </span>
                                <span class="slds-media__body" 
                                    style="padding-top: 9px;font-weight: 600;">
                                    <span class="slds-listbox__option-text slds-listbox__option-text_entity">
                                        {record.Name}
                                    </span>
                                </span>
                            </div>
                        </li>
                    </ul>
                </div>
        </div>
    </div>
</template>

--------------------------------------------------------------------------------------------------------------------------

lookupResultComponentLWC.js

import { LightningElement, api } from 'lwc';

export default class LookupResultComponentLWC extends LightningElement {

    @api record;
    @api iconName;

    handleSelect(event){
        event.preventDefault();
        const selectedRow = new CustomEvent('select',{detail : this.record.Id});
        this.dispatchEvent(selectedRow);
    }
}

--------------------------------------------------------------------------------------------------------------------------

ObjectManager.cls

public with sharing class ObjectManager {

    @AuraEnabled
    public static List<SObject> getRecords(String searchKey, String objectName, String searchField){
        String key = '%' + searchKey + '%';
        String QUERY = 'Select Id, '+searchField+' From '+objectName +' Where '+searchField +' LIKE :key';
        System.debug(System.LoggingLevel.DEBUG, QUERY);
        List<SObject> sObjectList = Database.query(QUERY);
        return sObjectList;
    }
}

Friday, March 13, 2020

Lightning Web Components: Use of Arrow functions

Weird! 'this' keyword appears not working in regular function(response){..}


Last month I was working on some requirement, where I was calling a regular function(response) {..} inside a callback method. It was weird for me that the scope variables were appearing as undefined every time I logged them.

connectedCallback(){         
    const messageCallback =function(response){             
        this.lead = response.data.sobject;                         
        this.leadName= this.lead.Name;       //undefined      
        this.leadStatus = this.lead.Status;   //undefined        
        const dispatchLeads = new CustomEvent('openutilitybar'); 
        this.dispatchEvent(dispatchLeads);         
    };

I tried to understand the scope of 'this' in a regular function(response){..}. After checking few community references and Mozilla Developer Guide, I found that regular functions have their own 'this' scope i.e this keyword represented the object that called the function, which could be the window, the document, a button or whatever.

To solve the issue, ES6 and the later versions have come up with Arrow functions i.e (response)=>{..}. Unlike regular functions, arrow functions do not have their own 'this'. The value of 'this' inside an arrow function remains the same throughout the life cycle of the function and is always bound to the value of this in the closest non-arrow parent function.

connectedCallback(){         
    const messageCallback = (response)=>{             
        this.lead = response.data.sobject;                         
        this.leadName= this.lead.Name;       //Smith   
        this.leadStatus = this.lead.Status;   //Open - Not Contacted      
        const dispatchLeads = new CustomEvent('openutilitybar'); 
        this.dispatchEvent(dispatchLeads);         
    };

References:



Sunday, March 1, 2020

Lightning Web Components: Use of Pubsub in LWC

Scenarios where fired event is required to handle on the same application page however in different components which aren't directly/indirectly related?

Like in aura we have application event, LWC is enriched with Pubsub module. All we just need to import Pubsub module and use its listener methods.


Component Bundles:
  1. furnitureOnRentDemo (Parent Comp )
  2. furnitureDemo (Child Comp)
  3. pubsubDemo (Comp not in any relation but on same page)
  4. pubsub (Salesforce provided module)
** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**

References:

  • Manish Choudhary LWC learning videos [Available in Udemy]


Snippet:


Code:

furnitureOnRentDemo.html

<template>
    <lightning-card title="Available Furnitures">
        <lightning-layout>
            <lightning-layout-item size="4" padding="around-small">
                <ul>
                    <template for:each={furnitureinfo} for:item="furniture">
                        <li key={furniture.name} style="padding: 10px">
                            <c-furniture-demo furnitureinfo={furniture} ></c-furniture-demo>
                        </li>
                    </template>
                </ul>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>

-------------------------------------------------------------------------------------------------------------------------

furnitureOnRentDemo.js

import { LightningElement, wire } from 'lwc';
import {CurrentPageReference} from 'lightning/navigation';

export default class FurnitureOnRentDemo extends LightningElement {
    furnitureinfo=[
        {name:'Crystal chairs',type:'chair'},
        {name:'Poly tables',type:'table'},
        {name:'Wooden furnitures',type:'sofa'},
        {name:'Iron furnitures',type:'bed'}
    ];
}

-------------------------------------------------------------------------------------------------------------------------

furnitureDemo.html

<template>
    <div class="slds-p-around_medium lgc-bg" onclick={tileClickHandler}>
        <lightning-tile label={furnitureinfo.name} >
            <p class="slds-truncate" title={furnitureinfo.type}>Furniture Type: {furnitureinfo.type}</p>
        </lightning-tile>
    </div>
</template>

-------------------------------------------------------------------------------------------------------------------------

furnitureDemo.js

import { LightningElement,api,wire } from 'lwc';
import {fireEvent} from 'c/pubsub';
import {CurrentPageReference} from 'lightning/navigation';

export default class FurnitureDemo extends LightningElement {
    @api furnitureinfo; 

    @wire(CurrentPageReference) pageReference;

    tileClickHandler(){

        fireEvent(this.pageReference, 'selectedtile', this.furnitureinfo);
    }
}

-------------------------------------------------------------------------------------------------------------------------

pubsubDemo.html

<template>
    <lightning-card title="Selected Furniture">
        You have seleteced : {selectedTile.name}
    </br>
        type : {selectedTile.type}
    </lightning-card>
</template>

-------------------------------------------------------------------------------------------------------------------------

pubsubDemo.js

import { LightningElement,track,wire } from 'lwc';
import {registerListener, unregisterAllListeners} from 'c/pubsub';
import {CurrentPageReference} from 'lightning/navigation';

export default class PubsubDemo extends LightningElement {
    @track selectedTile={};
    @wire(CurrentPageReference) pageRef;

    connectedCallback(){
        registerListener('selectedtile', this.furnitureSelectHandler, this);
    }
    
    disconnectedCallback(){
        unregisterAllListeners(this);
    }

    furnitureSelectHandler(payload){
        this.selectedTile = payload;
    }
}

-------------------------------------------------------------------------------------------------------------------------

pubsub.js

/**
 * A basic pub-sub mechanism for sibling component communication
 *
 * TODO - adopt standard flexipage sibling communication mechanism when it's available.
 */

const events = {};

/**
 * Confirm that two page references have the same attributes
 * @param {object} pageRef1 - The first page reference
 * @param {object} pageRef2 - The second page reference
 */
const samePageRef = (pageRef1, pageRef2) => {
    const obj1 = pageRef1.attributes;
    const obj2 = pageRef2.attributes;
    return Object.keys(obj1)
        .concat(Object.keys(obj2))
        .every(key => {
            return obj1[key] === obj2[key];
        });
};

/**
 * Registers a callback for an event
 * @param {string} eventName - Name of the event to listen for.
 * @param {function} callback - Function to invoke when said event is fired.
 * @param {object} thisArg - The value to be passed as the this parameter to the callback function is bound.
 */
const registerListener = (eventName, callback, thisArg) => {
    // Checking that the listener has a pageRef property. We rely on that property for filtering purpose in fireEvent()
    if (!thisArg.pageRef) {
        throw new Error(
            'pubsub listeners need a "@wire(CurrentPageReference) pageRef" property'
        );
    }

    if (!events[eventName]) {
        events[eventName] = [];
    }
    const duplicate = events[eventName].find(listener => {
        return listener.callback === callback && listener.thisArg === thisArg;
    });
    if (!duplicate) {
        events[eventName].push({ callback, thisArg });
    }
};

/**
 * Unregisters a callback for an event
 * @param {string} eventName - Name of the event to unregister from.
 * @param {function} callback - Function to unregister.
 * @param {object} thisArg - The value to be passed as the this parameter to the callback function is bound.
 */
const unregisterListener = (eventName, callback, thisArg) => {
    if (events[eventName]) {
        events[eventName] = events[eventName].filter(
            listener =>
                listener.callback !== callback || listener.thisArg !== thisArg
        );
    }
};

/**
 * Unregisters all event listeners bound to an object.
 * @param {object} thisArg - All the callbacks bound to this object will be removed.
 */
const unregisterAllListeners = thisArg => {
    Object.keys(events).forEach(eventName => {
        events[eventName] = events[eventName].filter(
            listener => listener.thisArg !== thisArg
        );
    });
};

/**
 * Fires an event to listeners.
 * @param {object} pageRef - Reference of the page that represents the event scope.
 * @param {string} eventName - Name of the event to fire.
 * @param {*} payload - Payload of the event to fire.
 */
const fireEvent = (pageRef, eventName, payload) => {
    if (events[eventName]) {
        const listeners = events[eventName];
        listeners.forEach(listener => {
            if (samePageRef(pageRef, listener.thisArg.pageRef)) {
                try {
                    listener.callback.call(listener.thisArg, payload);
                } catch (error) {
                    // fail silently
                }
            }
        });
    }
};

export {
    registerListener,
    unregisterListener,
    unregisterAllListeners,
    fireEvent
};