Background
In a recent project we had the requirement to provide search functionality on standard and custom objects within other lightning components, lightning pages (flexipages), and flows. As a developer your first instinct is that there must be a standard out of the search component for reuse. But alas that's not the case so I decided to write a reusable search component that can be leveraged and used in all the above scenarios. Below are the specifics to the solution.
CustomObjectSeachCmp (Github)
Demo
Solution
The solution is a simple lightning component that relies on SOSL to do the search and a lightning datatable to display the results. The main components are highlighted below.
CustomObjectSearchCmpLtngCtrl
The apex controller for the lightning component has two @auraenabled functions for fetching search results and getting field properties. There is a wrapper class (ColumnDefinition) to structure the field properties to be returned for the lightning data table.
public with sharing class CustomObjectSearchCmpLtngCtrl {
@AuraEnabled public static List<SObject> getSearchList(String searchKeyWord,
String objectApiName, String fields) {
String searchQuery = 'FIND \'' + searchKeyWord + '\' IN ALL FIELDS ' +
'RETURNING ' + objectApiName + '(' + fields + ')';
System.debug('searchQuery: ' + searchQuery);
List<List<SObject>> results = Search.query(searchQuery);
return results[0];
}
@AuraEnabled public static List<ColumnDefinition> getColumnDefinitions(String objectApiName, String fields) {
System.debug('getColumnDefinitions: ' + objectApiName + ' ' + fields);
List<String> fieldList = fields.replaceAll('\\s+', '').split(',');
List<ColumnDefinition> columnDefinitions = new List<ColumnDefinition>();
//Get Object API information
Schema.DescribeSObjectResult objectDescribe = Schema.describeSObjects(new String[]{objectApiName}).get(0);
//Get field API information from object field map
Map<String, Schema.SObjectField> fieldMap = objectDescribe.fields.getMap();
for(String field : fieldList) {
if(fieldMap.containsKey(field)) {
Schema.DescribeFieldResult fieldDescribe = fieldMap.get(field).getDescribe();
ColumnDefinition cd = new ColumnDefinition(fieldDescribe.getLabel(), field,
String.valueOf(fieldDescribe.getType()));
System.debug('cd: ' + cd);
columnDefinitions.add(cd);
}
}
return columnDefinitions;
}
public class ColumnDefinition {
@AuraEnabled
public String label;
@AuraEnabled
public String fieldName;
@AuraEnabled
public String type;
public ColumnDefinition(String label, String fieldName, String type) {
this.label = label;
this.fieldName = fieldName;
this.type = type;
}
}
}
Lightning Component (CustomObjectSearchCmp)
The lightning component shows the search results in a lightning datatable and has a couple of configuration fields to specify the object to search on and the fields to return in the results.
CustomObjectSearchCmp.cmp
<aura:component controller="CustomObjectSearchCmpLtngCtrl"
implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome"
access="global">
<aura:attribute name="objectName" type="String" access="public"/>
<aura:attribute name="fields" type="String" access="public"/>
<aura:attribute name="searchKeyword" type="String"/>
<aura:attribute name="searchResult" type="List"/>
<aura:attribute name="columns" type="List"/>
<aura:attribute name="Message" type="boolean" default="false"/>
<aura:attribute name="showSpinner" type="Boolean" default="false" />
<!--Handlers-->
<aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
<article class="slds-card slds-is-relative">
<aura:if isTrue="{!v.showSpinner}">
<lightning:spinner />
</aura:if>
<div class="slds-m-around_medium">
<!-- SEARCH INPUT AND SEARCH BUTTON-->
<lightning:layout>
<lightning:layoutItem size="3" padding="around-small">
<lightning:input value="{!v.searchKeyword}"
required="true"
placeholder="search .."
aura:id="searchField"
label="Search"/>
</lightning:layoutItem>
<lightning:layoutItem size="2" padding="around-small">
<lightning:button onclick="{!c.search}"
variant="brand"
label="Search"
iconName="utility:search"/>
</lightning:layoutItem>
</lightning:layout>
<!-- ERROR MESSAGE IF NOT RECORDS FOUND-->
<aura:if isTrue="{!v.Message}">
<div class="slds-notify_container slds-is-relative">
<div class="slds-notify slds-notify_toast slds-theme_error" role="alert">
<div class="slds-notify__content">
<h2 class="slds-text-heading_small">No Records Found...</h2>
</div>
</div>
</div>
</aura:if>
<!-- TABLE CONTENT-->
<div style="height: 300px">
<lightning:datatable
columns="{!v.columns}"
data="{!v.searchResult}"
keyField="Id"/>
</div>
</div>
</article>
</aura:component>
The design file just exposes the "objectName" and "fields" properties on the component to be configurable on a lightning page (flexipage).
<design:component>
<design:attribute name="objectName" label="Object" description="Enter SObject API Name here"
default=""/>
<design:attribute name="fields" label="Fields" description="Enter field list comma separated to include in results"
default=""/>
</design:component>
CustomObjectSearchCmpController.js
({
onInit : function(component, event, helper) {
helper.getFieldDefinitions(component, event);
},
search : function(component, event, helper) {
var searchField = component.find('searchField');
var isValueMissing = searchField.get('v.validity').valueMissing;
// if value is missing show error message and focus on field
if(isValueMissing) {
searchField.showHelpMessageIfInvalid();
searchField.focus();
} else {
// else call helper function
helper.getSearch(component, event);
}
}
})
CustomObjectSearchCmpHelper.js
({
getFieldDefinitions : function(component, event) {
// show spinner
component.set("v.showSpinner" , true);
var action = component.get("c.getColumnDefinitions");
action.setParams({
'objectApiName': component.get("v.objectName"),
'fields': component.get("v.fields")
});
action.setCallback(this, function(response) {
//hide spinner when response coming from server
component.set("v.showSpinner" , false);
var state = response.getState();
if (state === "SUCCESS") {
var columnDefinitions = response.getReturnValue();
component.set("v.columns", columnDefinitions);
} else if (state === "INCOMPLETE") {
alert('Response is Incompleted');
} else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
alert("Error message: " +
errors[0].message);
}
} else {
alert("Unknown error");
}
}
//hide spinner
component.set("v.showSpinner", false);
});
$A.enqueueAction(action);
},
getSearch : function(component, event) {
// show spinner
component.set("v.showSpinner" , true);
var action = component.get("c.getSearchList");
action.setParams({
'searchKeyWord': component.get("v.searchKeyword"),
'objectApiName': component.get("v.objectName"),
'fields': component.get("v.fields")
});
action.setCallback(this, function(response) {
//hide spinner when response coming from server
component.set("v.showSpinner" , false);
var state = response.getState();
if (state === "SUCCESS") {
var searchResults = response.getReturnValue();
console.log("searchResults: " + JSON.stringify(searchResults));
// if storeResponse size is 0 ,display no record found message on screen.
if (searchResults.length == 0) {
component.set("v.Message", true);
} else {
component.set("v.Message", false);
}
// set searchResult list with return value from server.
component.set("v.searchResult", searchResults);
} else if (state === "INCOMPLETE") {
alert('Response is Incompleted');
} else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
alert("Error message: " +
errors[0].message);
}
} else {
alert("Unknown error");
}
}
//hide spinner
component.set("v.showSpinner", false);
});
$A.enqueueAction(action);
}
})
Hopefully this component can be of assistance to other lightning developers that are looking for SObject specific search functionality. Feel free to suggest any improvements and reuse and modify the code to suit your needs. Cheers and Happy Coding!
Comments