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

Tuesday, February 4, 2020

Lightning Web Components: Quick Actions with LWC and Aura

In such scenarios where requirement states to show some prepopulated fields while creating record from quick actions.

As of now (Winter 20), we can't use directly LWC in Quick Actions. To use it first we need it to wrap LWC in Aura component and then later use that Aura component in Quick Action.

Components:
  1. baseContactAuraComp (Base Parent Comp -Aura)
  2. newContactCreateForm (Child Comp -LWC)
Quick Action Button on Account: Select the create new record option and action type as Lightning Component.

Prepopulated Fields: Account and Appointment Set.
** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**

Snippets:




baseContactAuraComp.cmp

<aura:component implements="force:lightningQuickAction, force:hasRecordId">
<c:newContactCreateForm recordId="{!v.recordId}" onclosepopup="{!c.navToNewRecord }"></c:newContactCreateForm>
</aura:component>   


baseContactAuraCompController.js

({
navToNewRecord : function(component, event, helper) {
var recordId = event.getParam('recordId');
console.log('recordId'+recordId);
var navEvt = $A.get("e.force:navigateToSObject");
navEvt.setParams({
"recordId": recordId,
"slideDevName": "related"
});
navEvt.fire();
}
})

newContactCreateForm.html

<template>
<lightning-card>
<lightning-layout>
<lightning-layout-item size="6" padding="around-small">
<lightning-card title="Create A New Contact">
<lightning-input name="First Name" label="First Name" onchange={contactFirstNameChangeHandler}></lightning-input>
<lightning-input name="Last Name" label="Last Name" onchange={contactLastNameChangeHandler}></lightning-input>
<lightning-input name="Account Name" value={accountName} label="Account"></lightning-input>
<lightning-input name="Appointment Set" label="Appointment Set" type="checkbox" data-id="checkbox"></lightning-input>
<lightning-button label="Save" onclick={createContact} variant="brand"></lightning-button>
</lightning-card>
</lightning-layout-item>
</lightning-layout>
</lightning-card>
</template>
newContactCreateForm.js
import { LightningElement, api, wire, track } from 'lwc';
import {getRecord, createRecord} from "lightning/uiRecordApi";
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
const fieldArray= ['Account.Id','Account.Name','Account.Appointment_Set__c'];
export default class NewContactCreateForm extends LightningElement {
@api recordId;
@track accountId;
@track contactFirstName;
@track contactLastName;
@track accountName;
@track appointmentSet;
@wire (getRecord, {recordId: '$recordId',fields: fieldArray})
result({data,error}){
if(data){
console.log('data.Name:'+data.fields.Name.value);
console.log('data.Appointment_Set__c'+data.fields.Appointment_Set__c.value);
this.accountId = data.fields.Id.value;
this.accountName = data.fields.Name.value;
const checkbox = this.template.querySelectorAll('[data-id="checkbox"]');
console.log('checkbox>>'+checkbox);
if(data.fields.Appointment_Set__c.value){
checkbox[0].checked = true;
}
this.appointmentSet = data.fields.Appointment_Set__c.value;
} else if(error){
this.showToast('ERROR', error.body.message, 'error');
}
}
createContact(event){
event.preventDefault();
const fields = { 'FirstName':this.contactFirstName,'LastName':this.contactLastName, 'AccountId':this.accountId, 'Appointment_Set__c':this.appointmentSet};
const recordInput = {apiName: 'Contact',fields};
if(window.confirm("Do you really want to save it?")){
createRecord(recordInput).then(response =>{
console.log('Contact has been created:', response.id);
const recordId= response.id;
this.showToast('SUCCESS','Contact is created'+response.id, 'success');
const closePopupEvent = new CustomEvent('closepopup', { detail: {recordId}});
this.dispatchEvent(closePopupEvent);
}).catch(error=>{
this.showToast('ERROR', error.body.message, 'error');
console.log('Error in creating Contact:', error.body.message);
})
}
}
contactFirstNameChangeHandler(event){
this.contactFirstName = event.target.value;
}

contactLastNameChangeHandler(event){
this.contactLastName = event.target.value;
}

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

Lightning Web Components: Pagination & Multiple Rows Actions

In such scenarios, where requirement states to enable or disable the number of records (Eg: Pricebook etc) from Salesforce UI.

Components:
  1. customDataTable (Parent Comp - base component)
  2. pageSizeSetupForm (Child Comp - to set page size)
  3. paginationSetupForm (Child Comp - to navigate first, last, next and prev pages)
  4. dataTableResults (Child Comp - to show results as data table)
  5. ContactManager (Apex classes with aura enabled methods)
** components are created as a part of proof of concept. it can be modified and optimized as per requirement.**

Snippets:




Code:

customDataTable.html:

<template>
<lightning-card title="Contact List">
<c-page-size-setup-form rows-to-skip={rowsToSkip} onpagesizeselect={pageSizeHandler}></c-page-size-setup-form>
<c-data-table-results page-size={pageSize} rows-to-skip={rowsToSkip} onselectedrows={selectedRowsHandler}></c-data-table-results>
<c-pagination-setup-form page-size={pageSize} rows-to-skip={rowsToSkip} onsetrowstoskip={rowsToSkipHandler}></c-pagination-setup-form>
</lightning-card>
</template>


customDataTable.js


import { LightningElement, track } from 'lwc';

export default class CustomDataTable extends LightningElement {

@track pageSize;
@track rowsToSkip;

@track firstPage;
@track lastPage;

pageSizeHandler(event){
const pageSize = event.detail.size;
this.pageSize = pageSize;
const rowsToSkip = event.detail.rowsToSkip;
this.rowsToSkip = rowsToSkip;
console.log('In3 customDataTable: this.pageSize>>'+this.pageSize);
console.log('In3 customDataTable: this.rowsToSkip>>'+this.rowsToSkip);
}

rowsToSkipHandler(event){
console.log('In rowsToSkipHandler : this.rowsToSkip>>'+this.rowsToSkip);
const rowsToSkip = event.detail;
this.rowsToSkip = rowsToSkip;
console.log('In rowsToSkipHandler : this.rowsToSkip>>'+this.rowsToSkip);
const dataTableResultsComp = this.template.querySelector('c-data-table-results');
if(dataTableResultsComp){
dataTableResultsComp.rowsToSkip = this.rowsToSkip;
dataTableResultsComp.getContacts();
}
}
selectedRowsHandler(){
const dataTableResultsComp = this.template.querySelector('c-data-table-results');
if(dataTableResultsComp){
console.log('In selectedRowsHandler :this.rowsToSkip>> '+this.rowsToSkip);
dataTableResultsComp.rowsToSkip = this.rowsToSkip;
dataTableResultsComp.getContacts();
}
}

get recordFound(){
if(!this.rowsToSkip){
console.log('In recordFound: true: this.rowsToSkip >>'+this.rowsToSkip);
return true;
}
console.log('In recordFound: false: this.rowsToSkip >>'+this.rowsToSkip);
return false;
}
}


pageSizeSetupForm.html:

<template>
<lightning-layout multiple-rows>
<lightning-layout-item padding="around-small">
<lightning-combobox name="pageSize" placeholder="Size" label="Size" variant="label-hidden" value={selectedValue} options={pageSize} onchange={pageSizeChangeHandler}>
</lightning-combobox>
</lightning-layout-item>
</lightning-layout>
</template>


pageSizeSetupForm.js:

import { LightningElement, track, api } from 'lwc';

export default class PageSizeSetupForm extends LightningElement {
@track pageSize =[{label:'100',value:'100'},{label:'500',value:'500'}];
@api rowsToSkip;
pageSizeChangeHandler(event){
const pageSize = event.target.value;
console.log('pageSizeSetupForm: pageSize >> '+pageSize);
console.log('pageSizeSetupForm: this.rowsToSkip >> '+this.rowsToSkip);
if(this.rowsToSkip){
console.log('In1 pageSizeSetupForm: pageSize >> '+pageSize);
console.log('In1 pageSizeSetupForm: this.rowsToSkip >> '+this.rowsToSkip);
var pageSizeSetup = { size : pageSize, rowsToSkip : this.rowsToSkip };
const pageSizeChangeEvent = new CustomEvent('pagesizeselect',{detail : pageSizeSetup , bubbles : true});
this.dispatchEvent(pageSizeChangeEvent);
} else{
console.log('In2 pageSizeSetupForm: pageSize >> '+pageSize);
console.log('In2 pageSizeSetupForm: this.rowsToSkip >> '+this.rowsToSkip);
var pageSizeSetup = { size : pageSize, rowsToSkip : 0 };
const pageSizeChangeEvent = new CustomEvent('pagesizeselect',{detail : pageSizeSetup , bubbles : true});
this.dispatchEvent(pageSizeChangeEvent);
}
}
}


dataTableResults.html:


<template>
<lightning-layout multiple-rows>
<lightning-layout-item padding="around-small">
<lightning-button label="Bingo!" variant="neutral" onclick={getSelectedHandler}></lightning-button>
</lightning-layout-item>
<lightning-layout-item padding="horizontal-small">
<lightning-datatable
key-field="Id"
data={results}
columns={columns}>
</lightning-datatable>
</lightning-layout-item>
</lightning-layout>
</template>


dataTableResults.js:


import { LightningElement, track, wire, api } from 'lwc';
import getAllContacts from '@salesforce/apex/ContactManager.getAllContacts';
import getSelectedRecords from '@salesforce/apex/ContactManager.getSelectedRecords';
import {ShowToastEvent} from 'lightning/platformShowToastEvent';

const columns = [
{ label: 'Name', fieldName: 'Name', type:'text' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' },
{ label: 'Account', fieldName: 'Account.Name',type:'text' },
{ label: 'Status', fieldName: 'Status__c',type:'boolean'}
];

export default class DataTableResults extends LightningElement {

/*@wire(getAllContacts, {numberOfRowsToReturn : '$pageSize' ,numberOfRowsToSkip: '$rowsToSkip'})
result({data,error}){
if(data){
this.results = data;
}else if(error){
this.showToast('ERROR', error.body.message, 'error');
}
};*/

privatePageSize;
privateRowsToSkip;
firstTimeRun = false;
selected;
//@api rowsToSkip;
@track results;
@track columns = columns;

@api
get rowsToSkip(){
return this.privateRowsToSkip;
}

set rowsToSkip(value){
console.log('In setting :value >>'+value);
if(value){
this.privateRowsToSkip = value;
} else{
this.privateRowsToSkip = '0';
}

console.log('In setting :this.privateRowsToSkip >>'+this.privateRowsToSkip);

}

@api
get pageSize(){
return this.privatePageSize;
}

set pageSize(value){
console.log('In setting: this.privatePageSize >>'+this.privatePageSize);
console.log('In setting: value >>'+value);
if(value){
this.privatePageSize = value;
} else{
this.privatePageSize = '10';
}
console.log('In setting: this.privatePageSize >>'+this.privatePageSize);
console.log('In setting: this.privatePageSize: checking this.firstTimeRun >>'+this.firstTimeRun);
if(this.firstTimeRun){
console.log('In this.getContacts() call>>');
this.getContacts();
console.log('Out this.getContacts() call>>');
}

}

connectedCallback(){
console.log('In connected call>>');
this.firstTimeRun =true;
console.log('In connected call: this.firstTimeRun >>'+this.firstTimeRun);
this.getContacts();
console.log('Out connected call>>');
}

@api
getContacts(){
console.log('In getContacts() : this.privatePageSize >>'+this.privatePageSize);
console.log('In getContacts() : this.privateRowsToSkip >>'+this.privateRowsToSkip);
getAllContacts({numberOfRowsToReturn : parseInt(this.privatePageSize) ,numberOfRowsToSkip: parseInt(this.privateRowsToSkip)}).then((con) =>{
this.results = con;
console.log('datatable>>'+this.results);
}).catch((error) =>{
this.showToast('ERROR', error.body.message, 'error');
})
}

getSelectedHandler(){
var el = this.template.querySelector('lightning-datatable');
this.selected = el.getSelectedRows();
console.log('this.selected>>'+this.selected);
this.disableStatus(this.selected);
}

disableStatus(selected){
for(var i=0;i<selected.length; i++){
console.log('get All selected >>'+selected[i].Id);
}
getSelectedRecords({conList: selected});
const getSelectedEvent = new CustomEvent('selectedrows');
this.dispatchEvent(getSelectedEvent);
}

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

ContactManager.cls

public with sharing class ContactManager {

@AuraEnabled
public static List<Contact> getAllContacts(Integer numberOfRowsToReturn, Integer numberOfRowsToSkip){
Database.QueryLocator qResult= Database.getQueryLocator([Select Id, Name, Phone, Account.Name,Status__c FROM Contact WHERE Status__c=true ORDER BY Account.CreatedDate ASC LIMIT 10000]);
Database.QueryLocatorIterator queryIterator = qResult.iterator();
List<Contact> conList= new List<Contact>();
while(queryIterator.hasNext()){
conList.add((contact) queryIterator.next());
}
List<Contact> resultList= new List<Contact>();
if(!conList.isEmpty()){
System.debug('Size conlist>> '+conList.size());
if(numberOfRowsToSkip+numberOfRowsToReturn < conList.size()){
for(Integer i=numberOfRowsToSkip;i<(numberOfRowsToSkip+numberOfRowsToReturn);i++){
resultList.add(conList.get(i));
}
} else{
Integer listSize= conList.size();
for(Integer i=numberOfRowsToSkip;i<listSize;i++){
resultList.add(conList.get(i));
}
}

System.debug('Size resultList >> '+resultList.size());
return resultList;
}
return null;
}

@AuraEnabled(cacheable=true)
public static Integer getContactsCount(){
return [SELECT count() FROM Contact WHERE Status__c=true LIMIT 10000];
}

@AuraEnabled
public static void getSelectedRecords(List<Contact> conList){
List<Contact> resultList = new List<Contact>();
for(Contact con: conList){
if(con.status__c)
con.status__c = false;
else
con.status__c = true;
resultList.add(con);
}
System.debug('resultList>>'+resultList);
update resultList;
}

}