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 what’s new here (Salesforce CLI and scratch orgs), we’re going to reuse code from a project right here on Trailhead, Develop an Account Geolocation App with Aura Components. We’ll repeat many of the steps from that project, but use the CLI at the appropriate moments. We provide all the code in this module so you don’t have to jump back and forth.
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 force-app/main/default
. 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.sfdx force:apex:class:create -n AccountSearchController -d force-app/main/default/classes
- Open
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, push (synchronize) your new code to the scratch org.
sfdx force:source:push
The output of theforce:source:push
command looks something like this:=== Pushed Source STATE FULL NAME TYPE PROJECT PATH ───── ───────────────── ────────── ─────────────────────────────────────── Add AccountSearchController ApexClass force-app/.../AccountSearchController.cls Add AccountSearchController ApexClass force-app/.../AccountSearchController.cls-meta.xml
Your scratch org is now updated with the AccountSearchController.
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.
sfdx force:lightning:event:create -n AccountsLoaded -d force-app/main/default/aura
- Open
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>
- Push your new code to the scratch org.
sfdx force:source:push
The output of theforce:source:push
command looks something like this:=== Pushed Source STATE FULL NAME TYPE PROJECT PATH ───── ───────────────────────────────── ──────────────────── ────────────────────────────────────────────────────── Add AccountsLoaded/AccountsLoaded.evt AuraDefinitionBundle force-app/.../AccountsLoaded/AccountsLoaded.evt Add AccountsLoaded/AccountsLoaded.evt AuraDefinitionBundle force-app/.../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:sfdx force:lightning:component:create -n AccountSearch -d force-app/main/default/aura
As you can see, all the required files are created. - Open
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
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
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 ); } })
- Push the new source to the scratch org.
sfdx force:source:push
=== Pushed Source STATE FULL NAME TYPE PROJECT PATH ───── ──────────────────────────────────────── ──────────────────── ────────────────────────────────────────────────────── Add AccountSearch/AccountSearch.auradoc AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.auradoc Add AccountSearch/AccountSearch.cmp AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.cmp Add AccountSearch/AccountSearch.cmp AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.cmp-meta.xml Add AccountSearch/AccountSearch.css AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.css Add AccountSearch/AccountSearch.design AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.design Add AccountSearch/AccountSearch.svg AuraDefinitionBundle force-app/.../AccountSearch/AccountSearch.svg Add AccountSearch/AccountSearchController.js AuraDefinitionBundle force-app/.../AccountSearch/AccountSearchController.js Add AccountSearch/AccountSearchHelper.js AuraDefinitionBundle force-app/.../AccountSearch/AccountSearchHelper.js Add AccountSearch/AccountSearchRenderer.js AuraDefinitionBundle force-app/.../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.sfdx force:lightning:component:create -n AccountList -d force-app/main/default/aura
- Open
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
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.
- Push the new code to the scratch org.
sfdx force:source:push