Build the Geolocation App Using Salesforce CLI
Learning Objectives
- Describe how you use the CLI to create an Apex class.
- Describe how you use the CLI command to create an Aura component.
What Are We Building?
We’ll build our feature—our geolocation app—by writing code locally, and then synchronizing it to our scratch org, where we can test it. We’ll work our way through Apex and several Aura components.
To keep our focus on Salesforce CLI and scratch orgs, we’re going to provide you the code for a geolocation app that uses Aura components.
This diagram shows a high-level overview of the component interactions:
This design makes your app flexible and easier to maintain. As your app and business requirements evolve, you can replace the component that displays the data without reworking the component that queries the data. This design also allows you to reuse these components independently in other applications.
- The Account Search component calls a server-side action to search for accounts.
- The Apex method returns the SOSL search results.
- The Account Search component fires an event notifying other components of the data.
- Components that handle the event display the data to the user.
Create the Account Search Apex Controller
In this step, you create an Apex controller that lets your Aura components retrieve a list of accounts with their location information. Apex classes are stored in a folder called classes
in the force-app/main/default
folder of your Salesforce DX project. You can use the CLI to quickly scaffold a new Apex class.
- From within the
geolocation
project directory, run this command from the root of your project.sf apex generate class --name AccountSearchController --output-dir force-app/main/default/classes
- Open
force-app/main/default/classes/AccountSearchController.cls
and replace the scaffold code with this code, then save the file.public with sharing class AccountSearchController { @AuraEnabled public static List<Account> searchAccounts( String searchTerm ) { List<Account> accounts = new List<Account>(); if ( String.isNotBlank( searchTerm ) ) { List<List<SObject>> searchResults = [ FIND :searchTerm RETURNING Account( Id, Name, Phone, Website, BillingStreet, BillingCity, BillingState, BillingPostalCode ORDER BY Name LIMIT 10 ) ]; accounts = searchResults[0]; } return accounts; } }
- Now, deploy (synchronize) your new code to your default scratch org which you created in the previous unit.
sf project deploy start
The output of theproject deploy start
command looks something like this:Deploying v58.0 metadata to test-t7e5za@example.com using the v58.0 SOAP API. Deploy ID: 0Af6t00000AN Status: Succeeded | ████████████████████████████████████████ | 1/1 Components (Errors:0) | 0/0 Tests (Errors:0) Deployed Source =============================================================================================================== | State Name Type Path | ─────── ─────────────────────── ───────── ─────────────────────────────────────────────────────────────────── | Created AccountSearchController ApexClass force-app/main/default/classes/AccountSearchController.cls | Created AccountSearchController ApexClass force-app/main/default/classes/AccountSearchController.cls-meta.xml
Create the Accounts Loaded Aura Event
Using the single responsibility design principle, we delegate the display of the search results by firing an event that is handled by the Account Map and Account List components you create in later steps.
- Create the event in the aura folder.
sf lightning generate event --name AccountsLoaded --output-dir force-app/main/default/aura
- Open
force-app/main/default/aura/AccountsLoaded/AccountsLoaded.evt
and replace the contents with this code, then save the file.<aura:event type="APPLICATION"> <aura:attribute name="accounts" type="Account[]"/> </aura:event>
- Deploy your new code to the scratch org.
sf project deploy start
The output of theproject deploy start
command looks something like this:Deploying v58.0 metadata to test-t7e5za@example.com using the v58.0 SOAP API. Deploy ID: 0Af6t00000AN Status: Succeeded | ████████████████████████████████████████ | 1/1 Components (Errors:0) | 0/0 Tests (Errors:0) Deployed Source =============================================================================================================== | State Name Type Path | ─────── ─────────────────────── ───────── ─────────────────────────────────────────────────────────────────── | Created AccountsLoaded AuraDefinitionBundle force-app/main/default/aura/AccountsLoaded/AccountsLoaded.evt | Created AccountsLoaded AuraDefinitionBundle force-app/main/default/aura/AccountsLoaded/AccountsLoaded.evt-meta.xml
Create the Account Search Aura Component
Next, we’re going to repeat much of this process to create an AccountSearch component. Aura component files are grouped together in bundles, which are stored in folders inside the force-app/main/default/aura
folder. Similar to Apex classes, you can scaffold an Aura component from the command line.
- Create the component in the
aura
folder:sf lightning generate component --name AccountSearch --output-dir force-app/main/default/aura
As you can see, all the required files are created. - Open
force-app/main/default/aura/AccountSearch/AccountSearch.cmp
and replace its contents with the following code, then save the file.<aura:component controller="AccountSearchController"> <aura:registerEvent name="accountsLoaded" type="c:AccountsLoaded"/> <aura:handler name="init" value="{!this}" action="{!c.onInit}"/> <aura:attribute name="searchTerm" type="String" default="San Francisco"/> <lightning:card title="Account Search" iconName="standard:search"> <div class="slds-form slds-p-around_x-small"> <lightning:input label="Search" variant="label-hidden" value="{!v.searchTerm}" placeholder="Search by name, phone, website, or address" onchange="{!c.onSearchTermChange}"/> </div> </lightning:card> </aura:component>
This component has an input field for the user to enter search terms, such as account name or address, and registers event handlers when the component is initialized or the search term changes.
- Open
force-app/main/default/aura/AccountSearch/AccountSearchController.js
and replace its contents with the following code, then save the file.({ onInit: function( component, event, helper ) { // proactively search on component initialization var searchTerm = component.get( "v.searchTerm" ); helper.handleSearch( component, searchTerm ); }, onSearchTermChange: function( component, event, helper ) { // search anytime the term changes in the input field var searchTerm = component.get( "v.searchTerm" ); helper.handleSearch( component, searchTerm ); } })
The client-side controller handles the component initialization event and when the search term changes. It calls the helper file to perform the search based on the user’s input.
- Open
force-app/main/default/aura/AccountSearch/AccountSearchHelper.js
and replace its contents with the following code, then save the file.({ // code in the helper is reusable by both // the controller.js and helper.js files handleSearch: function( component, searchTerm ) { var action = component.get( "c.searchAccounts" ); action.setParams({ searchTerm: searchTerm }); action.setCallback( this, function( response ) { var event = $A.get( "e.c:AccountsLoaded" ); event.setParams({ "accounts": response.getReturnValue() }); event.fire(); }); $A.enqueueAction( action ); } })
- Deploy the new source to the scratch org.
sf project deploy start
Deploying v58.0 metadata to test-t7e5za@example.com using the v58.0 SOAP API. Deploy ID: 0Af6t00880W7VdBCAV Status: Succeeded | ████████████████████████████████████████ | 1/1 Components (Errors:0) | 0/0 Tests (Errors:0) Deployed Source ==================================================================================================================== | State Name Type Path | ─────── ────────────── ──────────────────── ────────────────────────────────────────────────────────────────────── | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.auradoc | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.cmp | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.cmp-meta.xml | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.css | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.design | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearch.svg | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearchController.js | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearchHelper.js | Created AccountSearch AuraDefinitionBundle force-app/main/default/aura/AccountSearch/AccountSearchRenderer.js
Create the Account List Aura Component
Next, we create an Aura component that displays accounts in a data table. To know what data to show, it subscribes to the c:AccountsLoaded
event that you created in a previous step.
- Create the component in the
aura
folder.sf lightning generate component --name AccountList --output-dir force-app/main/default/aura
- Open
force-app/main/default/aura/AccountList/AccountList.cmp
and replace its contents with the following code, then save the file.<aura:component> <aura:handler event="c:AccountsLoaded" action="{!c.onAccountsLoaded}"/> <lightning:navigation aura:id="navigation"/> <aura:attribute name="rows" type="Map[]"/> <aura:attribute name="cols" type="Map[]"/> <lightning:card title="Account List" iconName="standard:account"> <lightning:datatable data="{!v.rows}" columns="{!v.cols}" keyField="Id" hideCheckboxColumn="true" showRowNumberColumn="true" onrowaction="{!c.onRowAction}"/> </lightning:card> </aura:component>
This component listens for the AccountsLoaded event and displays the event data in a table. As the user searches and finds different results, the list updates accordingly.
- Open
force-app/main/default/aura/AccountList/AccountListController.js
and replace its contents with the following code, then save the file.({ onAccountsLoaded: function( component, event, helper ) { var cols = [ { 'label': 'Name', 'fieldName': 'Name', 'type': 'text' }, { 'label': 'Phone', 'fieldName': 'Phone', 'type': 'phone' }, { 'label': 'Website', 'fieldName': 'Website', 'type': 'url' }, { 'label': 'Action', 'type': 'button', 'typeAttributes': { 'label': 'View details', 'name': 'view_details' } } ]; component.set( 'v.cols', cols ); component.set( 'v.rows', event.getParam( 'accounts' ) ); }, onRowAction: function( component, event, helper ) { var action = event.getParam( 'action' ); var row = event.getParam( 'row' ); if ( action.name == 'view_details' ) { var navigation = component.find( 'navigation' ); navigation.navigate({ 'type': 'standard__recordPage', 'attributes': { 'objectApiName': 'Account', 'recordId': row.Id, 'actionName': 'view' } }); } } })
The client-side controller’sonAccountsLoaded
function transforms the event data into the format expected by the<lightning:datatable>
component. TheonRowAction
function navigates to the account record of the row the user interacted with.
- Deploy the new code to the scratch org.
sf project deploy start