Skip to main content
Join the Agentforce Hackathon on Nov. 18-19 to compete for a $20,000 Grand Prize. Sign up now. Terms apply.

Build the Geolocation App Using Salesforce CLI

Learning Objectives

After completing this unit, you’ll be able to:
  • 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:Flow control diagram when user performs a search.

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.

  1. The Account Search component calls a server-side action to search for accounts.
  2. The Apex method returns the SOSL search results.
  3. The Account Search component fires an event notifying other components of the data.
  4. 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.

  1. From within the geolocationproject directory, run this command from the root of your project.
    sf apex generate class --name AccountSearchController --output-dir force-app/main/default/classes
  2. Open force-app/main/default/classes/AccountSearchController.clsand 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;
        }
    }
  3. 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 the project deploy startcommand 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 
    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.

  1. Create the event in the aura folder.
    sf lightning generate event --name AccountsLoaded --output-dir force-app/main/default/aura
  2. Open force-app/main/default/aura/AccountsLoaded/AccountsLoaded.evtand replace the contents with this code, then save the file.
    <aura:event type="APPLICATION">
        <aura:attribute name="accounts" type="Account[]"/>
    </aura:event>
  3. Deploy your new code to the scratch org.
    sf project deploy start
    The output of the project deploy startcommand 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.

  1. Create the component in the aurafolder:
    sf lightning generate component --name AccountSearch --output-dir force-app/main/default/aura
    As you can see, all the required files are created. AccountSearch folder expanded to show its files in Visual Studio Code
  2. Open force-app/main/default/aura/AccountSearch/AccountSearch.cmpand 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.

  3. Open force-app/main/default/aura/AccountSearch/AccountSearchController.jsand 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.

  4. Open force-app/main/default/aura/AccountSearch/AccountSearchHelper.jsand 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 );
        }
    })
  5. Deploy the new source to the scratch org.
    sf project deploy start 
You see the definition bundle deployed to the scratch org:
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.

  1. Create the component in the aurafolder.
    sf lightning generate component --name AccountList --output-dir force-app/main/default/aura
  2. Open force-app/main/default/aura/AccountList/AccountList.cmpand 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.

  3. Open force-app/main/default/aura/AccountList/AccountListController.jsand 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’s onAccountsLoaded function transforms the event data into the format expected by the <lightning:datatable> component. The onRowAction function navigates to the account record of the row the user interacted with.

  4. Deploy the new code to the scratch org.
    sf project deploy start
Share your Trailhead feedback over on Salesforce Help.

We'd love to hear about your experience with Trailhead - you can now access the new feedback form anytime from the Salesforce Help site.

Learn More Continue to Share Feedback