Creating Custom Tabs for Lightning Web Components

Create a Lightning web component and add it as a custom tab in the App Launcher and navigation menu.

  • Create a new Lightning web component:
    • Name: myComponent
  • Add a lightning-card to myComponent:
    • Title: Hello
  • Add a lightning-button to the Hello lightning-card (Note: make sure the button lives in the “actions” slot):
    • Label: New
  • Add a footer paragraph to the Hello lightning-card:
    • Text: Footnote
  • Add myComponent as a new custom tab:
    • Name: Words

Before you begin, you must update the configuration file to allow for custom tabs.

Copy the following code and paste it into notepad and save it as myComponent.js-meta.xml

<?xml version=”1.0″ encoding=”UTF-8″?>
<LightningComponentBundle xmlns=”http://soap.sforce.com/2006/04/metadata”&gt;
<apiVersion>46.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>

Use workbench to deploy the lightning component bundle to your org.

Record Page Component Visibility Rules Based on Device

Cloud Kicks has a requirement to make the recommendation component visible on a Sales Rep’s desktop. Build out a custom Lightning account page.

  • Create a new Lightning page:
    • Page type: Record Page
    • Label: Account Page
    • Object: Account
    • Template: Clone Salesforce Default Page > Account Grouped View Default
  • Add the Recommendations component to the page you created
  • Add a visibility filter on the Recommendations component:
    • Filter Type: Device
    • Field: Form Factor
    • Operator: Equal
    • Value: Desktop
  • Save and activate the page for all users
  • Assign the page as the org default

Editable Lightning Datatable Component

I wanted a nicer way to display related contacts on an account page layout.  This editable lightning datatable custom component does the trick!  I love it and it’s working out really well for us.

ContactDataTable Component

<aura:component implements=”force:appHostable,flexipage:availableForAllPageTypes,force:hasRecordId” access=”global” controller=”ContactDataTableController”>
<aura:attribute name=”data” type=”Object”/>
<aura:attribute name=”columns” type=”List”/>
<aura:attribute name=”recordId” type=”String”/>
<!– This attribute will hold the update records from data table–>
<aura:attribute name=”updatedRecord” type=”Object[]” />

<aura:handler name=”init” action=”{!c.doInit}” value=”{!this}”/>

<!– You must define keyField as ‘Id’ to save the record back in Salesforce ‘onsave’ attribute will executed when user clicks on save button –>
<!–<lightning:card >–>
<lightning:datatable
aura:id=”contactDataTable”
columns=”{! v.columns }”
data=”{! v.data }”
keyField=”Id”
onsave =”{!c.onSave}”
hideCheckboxColumn=”true”
onrowaction=”{! c.handleRowAction }” />
<!–</lightning:card>–>
</aura:component>

ContactDataTableController.js

({
/*
* This finction defined column header
* and calls getContacts helper method for column data
* editable:’true’ will make the column editable
* */
doInit : function(component, event, helper) {
component.set(‘v.columns’, [
{label: ‘Name Range’, fieldName: ‘Name_Range__c’, editable:’true’, type: ‘text’},
//{label: ‘Name’, fieldName: ‘Name’, editable:’true’, type: ‘text’},
{label: ‘Contact Name’, fieldName: ‘linkName’, type: ‘url’,
typeAttributes: {label: { fieldName: ‘Name’ }}},
{label: ‘Title’, fieldName: ‘Title’, editable:’true’, type: ‘text’},
{label: ‘Email’, fieldName: ‘Email’, editable:’true’, type: ’email’},
{label: ‘Phone’, fieldName: ‘Phone’, editable:’true’, type: ‘tel’},
{label: ‘Board Contact’, fieldName: ‘Board_Contact__c’, editable:’true’, type: ‘text’},

]);
helper.getContacts(component, helper);
},

/*
* This function is calling saveDataTable helper function
* to save modified records
* */
onSave : function (component, event, helper) {
helper.saveDataTable(component, event, helper);
}
})

ContactDataTableHelper.js

({
getContacts : function(component, event, helper) {
var action = component.get(“c.getContacts”);
action.setParams({
accountId: component.get(“v.recordId”)
//console.log(accountId)
});
action.setCallback(this,function(response) {
var state = response.getState();
if (state === “SUCCESS”) {
var records =response.getReturnValue();
component.set(“v.data”, response.getReturnValue());
records.forEach(function(record){
record.linkName = ‘/’+record.Id;
});
component.set(“v.data”, records);
}
});
$A.enqueueAction(action);
},

/*
* This function get called when user clicks on Save button
* user can get all modified records
* and pass them back to server side controller
* */
saveDataTable : function(component, event, helper) {
var editedRecords = component.find(“contactDataTable”).get(“v.draftValues”);
var totalRecordEdited = editedRecords.length;
var action = component.get(“c.updateContacts”);
action.setParams({
‘editedContactList’ : editedRecords
});
action.setCallback(this,function(response) {
var state = response.getState();
if (state === “SUCCESS”) {
//if update is successful
if(response.getReturnValue() === true){
helper.showToast({
“title”: “Record Update”,
“type”: “success”,
“message”: totalRecordEdited+” Contact Records Updated”
});
helper.reloadDataTable();
} else{ //if update got failed
helper.showToast({
“title”: “Error!!”,
“type”: “error”,
“message”: “Error in update”
});
}
}
});
$A.enqueueAction(action);
},

/*
* Show toast with provided params
* */
showToast : function(params){
var toastEvent = $A.get(“e.force:showToast”);
if(toastEvent){
toastEvent.setParams(params);
toastEvent.fire();
} else{
alert(params.message);
}
},

/*
* reload data table
* */
reloadDataTable : function(){
var refreshEvent = $A.get(“e.force:refreshView”);
if(refreshEvent){
refreshEvent.fire();
}
},
})

ContactDataTableController.apxc

public with sharing class ContactDataTableController {

@AuraEnabled
public static List<Contact> getContacts(Id accountId)
{
return [SELECT Id, Name, title, Email, Phone,Name_Range__c, Board_Contact__c FROM Contact WHERE AccountId =: accountId];
}

@AuraEnabled
public static boolean updateContacts(List<Contact> editedContactList){
try{
update editedContactList;
return true;
} catch(Exception e){
return false;
}
}
}

Omni-Channel Lightning Component

Salesforce Omni-Channel is great, but the one downside in our organization is that you have to manually set your status to available in order to receive chat or work requests.

I am in the midst of creating a Background Utility Item which will auto log you into omni-channel.

Omni-Channel Lightning Component

<aura:component implements=”lightning:backgroundUtilityItem” access=”global” >
<aura:handler name=”init” value=”{!this}” action=”{!c.doInit}”/>
<lightning:omniToolkitAPI aura:id=”omniToolkit” />
</aura:component>

Omni-Channel Lightning Component Controller

({
doInit: function(cmp, evt, hlp) {
window.setTimeout(
$A.getCallback(function() {

var omniAPI = cmp.find(“omniToolkit”);
omniAPI.setServicePresenceStatus({
statusId: “0N561000000027Y”,
callback: function(result) {
if (result.success) {
console.log(‘Set status successful’);
console.log(‘Current statusId is: ‘ + result.statusId);
console.log(‘Channel list attached to this status is: ‘ + result.channels);
} else {
console.log(‘Set status failed’);
}
}
});

}), 1000
)
},
})

This works like 90% of the time. There are some kinks which I’m still trying to work out.  For example, sometimes when you open service console you will get an error that says “Error logging into Omni-Channel”.  Not too sure why yet as it seems sporadic.  Clearing broswer cache and re-logging into service console does the trick.  If you have any ideas as to why this is the case, feel free to shoot me a message.

Work with the Lightning Map Component and Apex Inherited Sharing

This is the hands on challenge from the Platform Developer 1 Winter 19 Certification Maintenance.

UtilityClass

public inherited sharing class UtilityClass {
public static List<sObject> queryObjects(String theObject, List<String> theFields, List<String> theFilters, String sortField, String sortOrder) {
String theQuery = ‘SELECT ‘ + string.join(theFields, ‘,’);
theQuery += ‘ FROM ‘ + theObject;
boolean firstFilter = true;
for (String filter : theFilters) { //loop through the filters
String clauseToUse = (firstFilter) ? ‘ WHERE ‘ : ‘ AND ‘; //get the right clause
filter = filter.trim();
filter = filter.replaceAll(‘(\\s+)’, ‘ ‘); //remove white spaces
theQuery += clauseToUse + filter; //add the filter to the query
firstFilter = false; //changes the filter clause
}
if(!String.isEmpty(sortField)) {
theQuery += ‘ ORDER BY ‘ + sortField;
if(!String.isEmpty(sortOrder)) {
theQuery += ‘ ‘ + sortOrder;
}
}
String theQueryResult = string.escapeSingleQuotes(theQuery); //escapes the string
return database.query(theQueryResult);
}
}

TowerMapController Class

public inherited sharing class TowerMapController {
@AuraEnabled
public static List<Tower__c> getAllTowers() {
String theObject = ‘Tower__c’;
List<String> theFields = new List<String>{‘Id’, ‘Name’, ‘State__r.Name’, ‘Location__Latitude__s’, ‘Location__Longitude__s’};
List<String> theFilters = new List<String>(); //empty filter list
String sortField = ‘Name’;
String sortOrder = ‘ASC’;
List<Tower__c> allTowers = UtilityClass.queryObjects(theObject, theFields, theFilters, sortField, sortOrder);
return allTowers;
}
}

Towermap Lightning Component

<aura:component implements=”flexipage:availableForAllPageTypes” controller=”TowerMapController” access=”global” >
<aura:attribute name=”mapMarkers” type=”Object” access=”PRIVATE” />
<aura:attribute name=”markersTitle” type=”String” access=”PRIVATE” />
<aura:handler name=”init” value=”{!this}” action=”{!c.handleInit}”/>
<aura:if isTrue=”{!!empty(v.mapMarkers)}” >
<lightning:map
mapMarkers=”{! v.mapMarkers }”
markersTitle=”{! v.markersTitle }”
zoomLevel=”{!v.zoomLevel}” />
</aura:if>
</aura:component>

Lightning Component Controller

({
handleInit: function (component, event, helper) {
helper.initHelper(component, event, helper);
}
})

Lightning Component Helper

({
initHelper : function(component, event, helper) {
helper.utilSetMarkers(component, event, helper);
},
utilSetMarkers : function(component, event, helper) {
let action = component.get(“c.getAllTowers”);
action.setCallback(this, function(response) {
const data = response.getReturnValue();
const dataSize = data.length;
let markers = [];
for(let i=0; i < dataSize; i += 1) {
const Tower = data[i];
markers.push({
‘location’: {
‘Latitude’ : Tower.Location__Latitude__s,
‘Longitude’ : Tower.Location__Longitude__s
},
‘icon’: ‘utility:Tower’,
‘title’ : Tower.Name,
‘description’ : Tower.Name + ‘ Tower Location at ‘ + Tower.State__r.Name
});
}
component.set(‘v.markersTitle’, ‘Out and About Communications Tower Locations’);
component.set(‘v.mapMarkers’, markers);
component.set(‘v.zoomLevel’,5);
});
$A.enqueueAction(action);
}
})