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
};





Friday, February 21, 2020

Lightning Web Components: Use of Streaming API in LWC

During a proof of concept, had faced a situation where business requirement was to show some business evaluation data in Utility Bar on the basis of inserted/updated Lead record. I tried with few available options, however got no luck as Utility Bar doesn't handle lightning Events and LWC Pubsub model.

After understanding the Utility API, found Utility bar is not a part of current opened lightning application. It treated as a separate application entity. To pass information to Utility Bar, Streaming API is the option that can be efficiently applied. However as we know Streaming API works on channel subscription. Push topics are required to create with the name, that further used as a channel in subscription method of Streaming API.

Since Utility API feature is still not available in LWC components, am wrapping it in Aura component. Just to demonstrate the use of Streaming API in LWC, I have used it. In any other case I would have preferred Aura only to fulfill the requirement.

Note: Avoid using Utility API in aura init method.

I have coded a sample of the whole requirement:

Component Bundles:
  1. CustomUtilityAura (Base Component)
  2. CustomUtilityBar (LWC Component)
  3. Push Topic code (Anonymous Run )
** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**

Refrences:




Snippet:




Code:

customUtilityAura.cmp:

<aura:component implements="force:hasRecordId,force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,force:lightningQuickAction" access="global" >
        <lightning:utilityBarAPI aura:id="utilitybar" />
        <lightning:card>
                <c:customUtilityBar onopenutilitybar="{!c.openUtilityBar}"></c:customUtilityBar>
        </lightning:card>
</aura:component>

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

customUtilityAuraController.js

({
    openUtilityBar : function(component, event, helper){
           var utilityAPI = component.find("utilitybar");
            utilityAPI.openUtility();
            utilityAPI.getEnclosingUtilityId().then(function(response) {
              utilityAPI.setUtilityLabel({label : "LWC Sessions", utilityId : response});
              utilityAPI.setUtilityIcon({icon : "insert_tag_field", utilityId : response });
          }); 
    }
})

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

customUtilityBar.html

<template>
    <p>Lead Name: {leadName}</p>
    <p>Lead Status: {leadStatus}</p>
</template>

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

customUtilityBar.js

import { LightningElement, track, api } from 'lwc';
import { subscribe, unsubscribe, onError, setDebugFlag, isEmpEnabled } from 'lightning/empApi';

export default class CustomUtilityBar extends LightningElement {
    lead= [];
    @track leadName;
    @track leadStatus;
    channel = '/topic/LeadNotifications';
    @api
    connectedCallback(){
        let myComponent = this; // strange that sometimes 'this' behaves weirdly in connectedcallback.
        const messageCallback =function(response){
            myComponent.lead = response.data.sobject;
            myComponent.leadName= myComponent.lead.Name;
            myComponent.leadStatus = myComponent.lead.Status;
            const dispatchLeads = new CustomEvent('openutilitybar');
            myComponent.dispatchEvent(dispatchLeads);
        };

        subscribe(this.channel,-1,messageCallback).then(response =>{
            
            console.log('Subscribed to channel ', response.channel);
        });
    }
}

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

PushTopic (Anonymous Run):

PushTopic pushTopic = new PushTopic(); 
pushTopic.Name = 'LeadNotifications'; 
pushTopic.NotifyForOperationCreate = true;
pushTopic.NotifyForOperationUpdate = true;
pushTopic.NotifyForFields = 'Referenced';
pushTopic.ApiVersion= 48.0;
pushtopic.Query = 'Select Id, Name, Status From Lead WHERE Status = \'Open\''; 
insert pushTopic;




Friday, February 14, 2020

Lightning Web Components: lightning-input file type vs lightning-file-upload for file size greater than 25 MB and less than 100 MB

After Winter 19 release, API calls can't be made from client-side Aura/LWC component. You have to make API calls from server-side controllers rather than client-side code irrespective of whether it is a CSP Trusted Site.

In such scenarios where custom file upload is required when file size is needed to restrict with less than 100 MB and more than 25 MB. As of now such requirements can't be implemented. Apex has limit of 4 MB file upload. And chatter rest api can't be used directly on client side controller. Then what all options are left?

Lightning-file-upload is the only option left to upload bigger size file. However it can't be customized.

In my requirement, I am to share public url of  uploaded file on chatter. Attaching code:

Component Bundle:

  • fileUpload (Base component)
  • PostChatterLWCController (Apex class)
Add the component in detail page.


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

Snippets:



Code:

fileUpload.html


<template>
    <lightning-card title='File Upload' icon-name="custom:custom19">
        <lightning-input name="Expose to public" label="Expose to public" type="checkbox" data-id="checkbox"></lightning-input>
        <lightning-file-upload
                label="Attachments"
                name="fileUploader"
                accept={acceptedFormats}
                record-id={recordId}
                onuploadfinished={handleUploadFinished}
                multiple>
        </lightning-file-upload>
    </lightning-card>
</template>

fileUpload.js


import { LightningElement, api } from 'lwc';
// imported to show toast messages
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
import postChatter from '@salesforce/apex/PostChatterLWCController.postChatter';

export default class fileUpload extends LightningElement {
    @api recordId;
    // accepted parameters
    
    get acceptedFormats() {
        return ['.pdf', '.png','.jpg','.jpeg','.mkv'];
    }
    cdIdArrays =[];
    handleUploadFinished(event) {
        let strFileNames = '';
        
        // Get the list of uploaded files
        const uploadedFiles = event.detail.files;

        for(let i = 0; i < uploadedFiles.length; i++) {
            strFileNames += uploadedFiles[i].name + ', ';
            this.cdIdArrays[i] = uploadedFiles[i].documentId;
        }
        //console.log('cd>>'+cdIdArrays[0]+' '+cdIdArrays[1]);
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Success!!',
                message: strFileNames + ' Files uploaded Successfully!!!',
                variant: 'success',
            }),
        );;
        const checkbox = this.template.querySelectorAll('[data-id="checkbox"]');
        if(checkbox[0].checked){
            this.doPostChatter();
        }     
    }

    doPostChatter(){
        postChatter({parentRecordId : this.recordId ,contendDocIdList: this.cdIdArrays}).then((con) =>{
            this.results = con;
            console.log('datatable>>'+this.results);
        }).catch((error) =>{
            this.showToast('ERROR', error.body.message, 'error');
        })
    }
}

PostChatterLWCController.cls



public inherited sharing class PostChatterLWCController {

    

    @AuraEnabled

    public static void postChatter(Id parentRecordId, List<Id> contendDocIdList){

    

        List<ContentVersion> cvList = [select id,contentdocumentid from contentversion where contentdocumentid IN: contendDocIdList];
        List<ContentDistribution> cdList = new List<ContentDistribution>();
        for(ContentVersion idObj : cvList){
            ContentDistribution cd = new ContentDistribution();
            cd.Name = 'Doc'+idObj.contentdocumentid;
            cd.ContentVersionId = idObj.id;
            cd.PreferencesAllowViewInBrowser= true;
            cd.PreferencesLinkLatestVersion=true;
            cd.PreferencesNotifyOnVisit=false;
            cd.PreferencesPasswordRequired=false;
            cd.PreferencesAllowOriginalDownload= true;
            cdList.add(cd);
        }
        insert cdList;
        List<FeedItem> FeedItemList= new List<FeedItem>();
        List<ContentDistribution> ContentDistributionList= [Select id, ContentDocumentId, DistributionPublicUrl, ExpiryDate FROM ContentDistribution WHERE ContentDocumentId IN: contendDocIdList];
        for(ContentDistribution cdObj: [Select id, Name, ContentDocumentId, DistributionPublicUrl, ExpiryDate FROM ContentDistribution WHERE ContentDocumentId IN: contendDocIdList]){
            FeedItem post = new FeedItem();
            post.parentid = parentRecordId;
            post.Body = UserInfo.getUserId()+' has posted public link to the doc: '+cdObj.Name+' Link: '+cdObj.DistributionPublicUrl;
            FeedItemList.add(post);
        }
        insert FeedItemList;
    }
}