Building a Real-Time Filtered List with Custom Lightning Components

Amy Lee
Amy Lee
Oct 18, 2018 · 18 min read
Typing into the filter box will filter the Contacts in real-time

Every few years I find myself having to learn new development skills: this time I was using Salesforce’s Lightning Components to make a custom Lightning app. Having been one of the original developers of the Lightning Design System (SLDS), I wondered how difficult it might be coming from my current world of React and Node.

TL;DR: it’s different and familiar at the same time. Some bits are easier in Lightning.

Doing the Lightning Trailhead tutorials is one thing, and writing your own app from scratch is quite another. What follows are my experiences in getting started, and the code that you too can use! (It’s long, but worth it.)

React/Node/Webpack vs Click-based app development

The Mental Shift

While it is definitely a change of thinking, there are some really nice things that you get with Lightning:

  • no software to install
  • no-fuss browser front-end to server backend AJAX
  • no database setup
  • leverage pre-built Lightning Components and standard Salesforce design tokens
  • totally componentized
  • Salesforce CLI + DX (developer experience) if you want
  • mobile responsive for free
  • React-style state/render
  • security all the way down

There’s no build chain to set up, no database to provision. Nope, you just create a Salesforce account in your web browser and that’s it. I even coded straight up in the built-in Developer Console! 😱

OK … it wasn’t all roses:

  • you have to read a lot of documentation
  • you will Google every error and try to figure out the “Lightning way” or “Apex way” of doing things — even if you’re experienced with relational databases or adept at web dev
  • you’re using JavaScript … but not really
  • you’re using Java … but not really
  • you’re using SQL … but not really
  • you’re using CSS … but not really
  • a component is really a package of related markup/scripts/styles/tokens
  • getting your head around what a Salesforce “object” is
  • getting your head around what an “app” is
  • endless clicking around the Setup screens
Architecture of this example

The Bird’s-Eye View

What I wanted to do was a simple training exercise:

  • Create a custom page (shown in a Tab) in a custom Lightning App
  • Create a custom Lightning Component that showed Contact object records
  • Load the data from Salesforce via an Apex class
  • Sort the data
  • Filter the data (with sanitized input) using user input
  • Navigate to the Contact object when you click the name

The architecture ended up being this:

  • Custom Lightning App: This creates the app “tile” in the global app switcher as well as a place to hold the Filtered Contacts Page (as a tab) and the Contacts sObject tabs
  • Custom Lightning Tab: To hold the Page
  • Custom Lightning Page: The real layout of the UI. I used the Lightning App Builder to configure the layout and add the components
  • Custom Component: The component is part markup template, part UI controller, part styles, and part frontend for the backend Apex data class. It rendered the filters and the list. Changes to the filter make calls over the wire to the Apex class and when the data comes back all we have to is update the component “attribute” which causes a render cycle — this is React-ish.
  • Apex controller class: This the handler that gets called from the Component and interfaces with the Salesforce database. I used SOQL to grab and sort Contact records.
  • Contacts: The Contact sObject (we call our object tables “sObjects”) holds records (aka “rows”). This is one of the “standard objects” that ships with Salesforce, and the Developer Edition ships with dummy data.
  • Org-level design tokens: A necessary Lightning Resource to let us access the Standard Design Tokens

Let’s take the journey step by step …

Registering for a Salesforce dev org

Step 1: Create a Developer Edition “org”

We call the account you log into an “organization” here, or “org” for short. The reason is because you end up eventually inviting all your colleagues into the same account (with different login usernames) and then you can all share the same database.

Getting an org provisioned is about a 3-minute process:

  1. Sign up at: https://developer.salesforce.com/
  2. Check your email a few minutes later
  3. That’s it! 🏆

When you log in that very first time you end up in a default Lightning-style org. If you click around, you probably run into the dummy data, even the Contacts objects.

Create an Apex class via Setup > Developer Console > File > New > Apex Class

Step 2: Make the Apex data controller class

This is the server-side bit that handles requests coming in from the custom component. You just have to name the component something sensible and list the AJAX parameters you want to deserialize.

I know it’s not everyone’s favorite editor, but you can use the built-in Developer Console by clicking the “setup gear” in the top bar and choosing the “Developer Console”. (Here’s the thing about Salesforce: these basic tools are part of the app so all you need is a web browser. Then when you log in from a different browser, the exact state of the developer UI is restored. Cool, eh?) Then click File > New > Apex class. I named mine: TestContactListController1.

Inside the controller I made a method called: public static List<Contact> allContacts(String nameFilterString). That takes in a string filter and returns a list of Contact objects. I also added a sanitizer to remove all non-word characters and turn them into wildcards. I’m not worried about SQL/SOQL-injection here, but it does make the user experience better.

Finally the SOQL code also orders by the Name field.

public with sharing class TestContactListController1 {
static String sanitizeQueryString(String aQuery) {
if (aQuery == null) return ‘%’;

String trimmedQuery = aQuery.trim();
if (trimmedQuery.length() == 0) return ‘%’;

return ‘%’ + trimmedQuery.replaceAll(‘\\W+’, ‘%’) + ‘%’;
}

@AuraEnabled
public static List<Contact> allContacts(String nameFilterString) {
String composedFilter = sanitizeQueryString(nameFilterString);
return [
SELECT Id, Name
FROM Contact
WHERE Name LIKE :composedFilter
ORDER BY Name ASC
];
}
}

A couple of nuances:

  • public with sharing class: this is required to make it visible to the Lightning component later
  • @AuraEnabled public static: is required to expose it to Lightning
  • Salesforce uses List<T> types instead of arrays
  • SOQL syntax is used instead of SQL
  • Using static bind variables (e.g. :composedFilter) is the only reliable way to do string regexes and concatenations. If you’re not used to relational databases the SOQL query gets parsed but :composedFilter will turn into a variable value placeholder — which is far better and safer than concatenating the SOQL queries like: “SELECT Id,Name FROM Contact WHERE Name LIKE ‘ ” +nameFilterString+“ ’ ORDER BY Name ASC”.
The “classic” Salesforce Aloha UI, Sales App selected, Contacts Tab selected

Step 3a: Making the Page

Salesforce has a long history of supporting custom development. So why the naming and organization of some things isn’t super obvious if you’re coming at it from a fresh all-Lightning 2018 perspective.

Contact record view mode (left), edit mode (right)

Once upon a time … Salesforce was pretty much a fancy form editor that had two modes: view a record, or edit that record. These were full page loads, not a Single Page Application (SPA). And back then it was Visualforce pages. The Salesforce UI back in the day had a row of tabs just under the top header, and you could make a “page” that had a clickable “tab”. (You could have pages that were fullscreen so, yes there are pages that didn’t have a tab counterpart.) Some of those tabs were just the names of objects in the system like Accounts, Notes, Leads, and they would happily sit alongside your custom tabs. Today you can use Lightning components — and Lightning Apps are just big components — but you need to fit it into the old model somewhat.

To make the page you go to: Setup > User Interface > Lightning App Builder > Lightning Pages > New. Then once you’re in the builder UI:

Choosing the page layout for your app
  • Click “App Page”
  • Give your page a name (label) like “Contact Filter Page”
  • Pick a layout like “Main Column and Right Side Bar”
  • Add some Lightning component for now so you can save the page, like “Rich Text”
  • Save > Activate > Save > Finish > Back

Step 3b: Making the Tab

Now we can add a tab to the top bar so we can navigate to it later. While in Setup navigate: Setup > User Interface > Lightning Page Tabs > New. Next, the more fun part:

  • Lightning Page: select the page you just made like “Contact Filter Page”
  • Tab Page: “Filtered Contacts”
  • Click on Tab Name to autopopulate
  • Select a Tab Style
  • Next
  • Save (for all profiles)

Step 3c: Making the App

Naming is hard. You might have noticed that there are many things called “app” and “Lightning” at this point. Don’t worry, you’ll get it. 😉 Click Setup > Apps > App Manager > New Lightning App.

Reordering the Lightning Apps in the App Switcher menu
  • App Name: enter “Filtered Contacts” (yes, I know you just made a Tab called the same thing)
  • App Branding: upload a thumbnail image if it you like
  • Next
  • Next
  • Next
  • Select the tab you just made, “Filtered Contacts”, and the “Contacts” object and press the right-add arrow
  • Next
  • Highlight all 30-something profiles
  • Save & Finish

When you click the app switcher in the upper left you should be able to see your new icon (or “FC” if you didn’t upload an image). You can even drag it to the top left for easy access. Click it to see the page you just created (on the tab in the new app).

Step 4: Update the Tokens Resource

You should already have a resource called “defaultTokens” you can open with File > Open Lightning Resources > “c:defaultTokens”. (If you don’t have it, then make a new one with File > New > Lightning Tokens. Naming matters, so call it exactly “defaultTokens”.)

Change the contents of the defaultTokens.tokens file to just:

<aura:tokens extends=”force:base”>

</aura:tokens>

This is a bit of magic that lets you use the SLDS Standard Tokens.

Custom Lightning component design

Step 5a: Create the Component

Finally, we get to make magic happen. A bit of coding now has to happen because we’re going to do a custom user interface. We’re going to revisit the Developer Console (open it from the Setup gear icon if you closed it).

New Lightning Component dialog box

Clicking File > New > Lightning Component brings up the dialog to add your new component. And then you should be back in the text editor with a very empty <aura:component> waiting for your custom code!

Let’s start out by cutting and pasting the following into the “COMPONENT” tab, then we’ll explain the parts of it. (Click “COMPONENT” on the right of the Developer Console if it’s not already selected.)

<aura:component controller=“TestContactListController1" access=“global” implements=“flexipage:availableForAllPageTypes”>
<aura:attribute name=“contacts” type=“Contact[]”/>
<aura:handler name=“init” value=“{!this}” action=“{!c.init}” />

<lightning:card title=“Filtered Contacts”>
<div class=“search-field”>
<lightning:input aura:id=“nameFilter” label=“Filter names” onchange=“{!c.handleNameFilterChange}” />
</div>
<div class=“results”>
<aura:if isTrue=“{!v.contacts.length > 0}”>
<p class=“has-results”>
Showing {!v.contacts.length} contact(s):
</p>
<ol class=“slds-list_dotted”>
<aura:iteration items=“{!v.contacts}” var=“contact”>
<li>
<a class=“contact-name” onclick=“{!c.handleClickContactName}” data-sfid=“{!contact.Id}”>
{!contact.Name}
</a>
</li>
</aura:iteration>
</ol>
<aura:set attribute=“else”>
<p class=“no-results”>Nothing to show.</p>
</aura:set>
</aura:if>
</div>
</lightning:card>
</aura:component>

The first tag informs Salesforce this markup template has logic defined in our Apex controller, TestContactListController1. Admittedly, this gets confusing because on the right side of the Developer Console is another button saying “CONTROLLER”, but we’re talking about the server-side Apex here. (More on this later.) Access global means other code can see this component, and flexipage:availableForAllPageTypes lets this custom component show up in the Lightning App Builder page editor.

The aura:attribute is a special data-binding variable. When its value changes, the component UI will repaint. (Sound familiar, React devs?) In this case it will hold the matching Contact objects so we can iterate over them.

The aura:handler for initialization will run the init() method when the component is first added to the DOM.

Example Base Lightning card component

Next comes the first use of a Base Lightning Component: <lightning:card title=“Filtered Contacts”>. The nice thing about Base components is that they are styled with the latest version of SLDS and they handle accessibility concerns — you just supply the right attributes and body content. That way, when future versions of the Lightning UI might put actions buttons in a different place or do a different style treatment to the footers, you don’t have to do any recoding yourself!

For example, the <lightning:input aura:id=“nameFilter” label=“Filter names” onchange=“{!c.handleNameFilterChange}” /> does so much for you it’s crazy. It’s not just an HTML input tag, this handles data binding, automatic label placement, number formatting, action handlers, and more. You’ll note though that we can give it an aura:id so we can find it later, and we’ll set our change event handler to handleNameFilterChange.

We’ll speed through the rest of the component because it’s pretty obvious. The <aura:if> tag lets you do conditional logic, though the else-clause has to use the special <aura:set> syntax. (A little verbose if you ask me.) There’s an <aura:iteration> that lets you loop through all items in the Contact object array, setting an iterator variable called contact inside the loop.

Finally, you’ll note the <a> doesn’t use an href because hardcoding URLs in Salesforce is a serious no-no — URLs differ depending on whether the UI is in classic mode or Lightning mode, or on mobile-responsive mode, or in the Salesforce mobile app (née Salesforce1). Instead we’ll use an onclick handler! We do pass some context info about the sObject ID as a data-sfid parameter.

Step 5b: Create the Component Controller

Click on the “CONTROLLER” tab and type this in:

({
init: function(component, event, helper) {
helper.loadList(component);
},
handleClickContactName: function (component, event, helper) {
const target = event.currentTarget;
const sfid = target.dataset.sfid;

var navigateEvent = $A.get(“e.force:navigateToSObject”);
navigateEvent.setParams({
“recordId”: sfid
});
navigateEvent.fire();
},
handleNameFilterChange: function (component, event, helper) {
helper.loadList(component);
}
})

Is this JavaScript? Sort of. The way you access objects is a bit different, so just pay attention to the nuances. 👀

To populate the contacts attribute we’ll use a helper. (Due to historical reasons if you want to DRY [Don’t Repeat Yourself] up your code you have to put reusable bits in the “HELPER”. You cannot call a controller method from another controller method.) Again, naming is hard. The “CONTROLLER” tab is for this component’s markup, not the Apex API controller that will serve us the Contact data via AJAX. Just nod your head, accept it, and move on… 😿

On initialization and when we detect the name filter input changes, we’ll call loadList. And then there’s that <a> link handler we just talked about, handleClickContactName, that has to get the Salesforce object ID from the link element’s dataset. Because Salesforce has a flexible routing system that works on the web and on devices, you have to fire a navigation event: force:navigateToSObject.

👉 Note: you’ll notice that there’s prefixes for the names of things. Variable attributes have “v.” prefixes, components use “c.”, and events use “e.”. There are other namespaces like “force:”, “aura:”, and “lightning:”. It’s just the way it is.

Step 5c: Create the Helper

About that AJAX. Our component will need to call the Apex server-side method, pass it some params, then update our attributes when the data comes back. Click on “HELPER” and add this:

({
loadList: function(component) {
const action = component.get(“c.allContacts”);
const nameFilterString = component.find(“nameFilter”).get(“v.value”);
action.setParams({
nameFilterString: nameFilterString
});
action.setCallback(this, function(a) {
component.set(“v.contacts”, a.getReturnValue());
});
$A.enqueueAction(action);
}
})

The first thing you’ll notice is we call c.allContacts, but there isn’t a “CONTROLLER” method called allContacts(). So … where … ? 😳 Recall that in the “COMPONENT” we set the “controller” to the Apex class TestContactListController1. Oh. That class is where we defined it.

👉 Note: here again is a naming-is-hard problem. The helper needs to call the component’s controller, which is an Apex class. The component’s markup however uses the “CONTROLLER” tab, which is different. Just deal with it. 🕶

Getting the value out of the name filter field is via its aura:id, and we use component.find().get(). Then we use the magic Aura utility $A to fire our data action.

Painter with a multicolor palette and the STYLE brush of power, happy trees in background

Step 5d: Create Component Styles the Right Way (tm)

The home stretch! Type this into the “STYLE” and then we’ll talk:

.THIS .contact-name {
font-weight: token(fontWeightBold);
}

.THIS .has-results {
color: token(colorTextWeak);
font-weight: token(fontWeightBold);
margin-bottom: token(spacingSmall);
}

.THIS .no-results {
color: token(colorTextError);
}

.THIS .results {
padding: token(spacingMedium);
}

.THIS .search-field {
padding: token(spacingMedium);
}

First, no hard-coded pixel values are used. This is super important because as the Lightning UI changes over time (due to SLDS updates) you want to take advantage of our automatic updates. We have a large list of them available in Lightning on developer.salesforce.com.

Lightning App Builder but with our new custom Lightning component

Step 6: Add the Custom Component to the Page

Back on the Salesforce UI (not the Developer Console), go back to your Filtered Contacts Page. If everything worked out right, we should see magic happen: click on the Setup gear at the top > Edit Page. On the left we should see the FilteredContacts component. Just drag it onto the page, click Save, then Back.

As you type, the onchange events fire which cause your component to ping the Apex backend. The results are filtered (and sanitized) in the custom SOQL query’s LIKE clause, then returned to the component helper. That updates the component attribute, which causes a rerender, which then iterates through the results. And if you click on a name link, a navigation event fires and the Salesforce router will send you to the Contact record.

Phew. Give yourself a hand!

You made it! Have a GIF.

Epilogue

A lot of internet searching went into figuring this out. I think partly it’s because a lot has changed since the first days that I did Trailhead tutorials.

But there’s also combinations of the above that were confusing — the result of a fast-moving ecosystem. Searches included:

  • “Where do I make a new Lightning app?”
    Setup > Apps > New Lightning App
  • “I forgot to select Available Items, how do I add them?”
    Setup > Apps > App Manager > [click your app] > Select Items
  • “Why doesn’t my custom Lightning component show up in Available Items?”
    Because you need a tab
  • “Which user profiles do I have to select?”
    All of them
  • “What’s the difference between App>App Manager and User Interface>Lightning App Builder”
    App Manager makes an app, Lightning App Builder configures the page
  • “What’s the difference between an app, a tab, and a page?”
    An App can show up in the App Switcher modal or go fullscreen, a Tab is what you select in the App’s Select Items menu, and a page is the layout that holds components
  • “Where do I set up a Lightning tab?”
    Setup > User Interface > Tabs
  • “Why are some setup menu options in App and others in User Interface?”
    Historical reasons
  • “How do I make a user interface with columns and draggable components?”
    That’s on a Page and you use the Lightning App Builder
  • “I don’t like the Page layout, how can I change that?”
    Unfortunately once you create a Page and set its column layout you’re stuck. Make a new version of the page, drag the components back in
  • “How do I change the Page shown on the Lightning Tab?”
    Sorry, you can’t change that either once you make the Tab. You’ll need to make a new Tab.
  • “How do I make a new Lightning component?”
    In the Developer Console, or if you have an Enterprise-level org you can use a “Dev Hub” and SFDX
  • “Why doesn’t my Lightning component show up in the Lightning App Builder page editor?”
    Probably because your component needs the attributes:
    access=“global” implements=“flexipage:availableForAllPageTypes”
  • “Where do I put the scripts that handle input field changes, button clicks, and other Lightning events?”
    In the “CONTROLLER” of the component
  • “How do I call a ‘CONTROLLER’ method from another method?”
    You can’t, but you can do that in the “HELPER”
  • “How do I get the value of a component’s attribute?”
    Uuse component.get(“v.someAttribute”)
  • “How do I get the value of a form field?”
    On the field use the aura:id to name it (and yes you should probably set the normal name attribute), then in your “CONTROLLER” do:
    component.find(“theAuraId”).get(“v.value”)
  • “How do I navigate a link?”
    Use an onclick handler and in the “CONTROLLER” use a $A.get(“e.force:navigateToSObject”) event
  • “sObject vs object?”
    I use them interchangeably. I think the reason we call them “sObjects” is because they’re really like fancy views where some columns are derived from multiple data and there’s a lot of automatic foreign key management going on for you
  • “SQL vs SOQL?”
    SOQL is really it’s own thing but has a similar SELECT-FROM-WHERE-ORDER sequence
  • “How do I match case-insensitive values?”
    It depends on the field type of it is case-sensitive. My suggestion would be to 1) add a normalized value column, and 2) add an Apex trigger that fires on field changes puts values into the normalized column
  • “How do I make complex LIKE clauses with variables?”
    It’s hard to do that in the query, so I would use bind variables: in the SOQL add a :variable, and just before the SOQL define it
  • “How do I guard against SQL injection?”
    Bind variables
  • “I get confused between Apex controllers and Lightning component controllers!”
    Me too. They both use the “c.” prefix.
  • “What is the difference between Apex, Visualforce, Aura, and Lightning?”
    Apex are server-side functions that can take in parameters and return values, run SOQL queries, and fire events. It looks a little like Java. Visualforce is a templating language that can call Apex. It looks a little like Angular. Aura is a components-based UI framework where each component has separate files for markup, styles, logic, and tokens. Lightning is a flavor of Aura.
  • “What about the namespaces like force:, aura:, and lightning:?”
    It is just that way
  • “Can I use HTML in my template or am I limited to Lightning components?”
    You can write most HTML, but some syntax isn’t supported like SVG xlink. Use the <lightning:icon> component
  • “Can I use normal JavaScript DOM events?”
    Maybe. I have only tried onclick and onchange. Lightning manages the DOM for you and so you might only be able to use handlers it knows about.
  • “Can I mix React into my component?”
    Yes but it is tricky. I believe you have to upload the compiled React as a static resource, then call it. Also remember that Lightning manages the DOM lifecycle and that can be complex.
  • “I don’t like using the Developer Console. How can I edit the components?”
    There are plugins for Sublime or Visual Studio Code like Mavensmate.
  • “Can I use Git with Salesforce?”
    Yes, but a little bit of work. (I do this.) On an enterprise-level org, you can download the code from you Dev Hub with SFDX — or you can get a 30-day trial org with Dev Hub enabled. Then you can sync that to Github. To run the code I set up a scratch org and push my local code to it.
  • “I made my custom component but it still doesn’t show up in the Lightning App Builder!”
    Make sure it has global access and implements the “flexipage” types. To get the page up on the UI you need to make a tab. Then you need to put the tab in an app. Then you need to grant permissions. And make sure to activate!
  • “But I still can’t see my custom component!”
    Probably because My Domain isn’t enabled
  • “Why can’t I use the token( ) helper in my Lightning component STYLE?”
    You first have to create a Lightning Resource that extends the Aura base tokens called defaultTokens.tokens and then add the <aura:tokens extends=“force:base”>
  • “Why use token( ) instead of hard-coded pixel values in the STYLE?”
    Because the Lightning UI evolves over time and token() will use the latest values
  • “How often does the UI change? How often do Design Tokens get updated?”
    It varies, so either subscribe to the design-system repo on GitHub or watch the SLDS Releases page
  • “How can I override some of the design token values in my defaultTokens.tokens?”
    When you extend the force:base tokens you can then override them with: <aura:token name=”colorButtonBrand” value=”#4488cc"/>
  • “What’s the difference between SLDS components and the Base Lightning Components?”
    SLDS Component Blueprints are the recommended markup to use with the current SLDS CSS, but it is just static markup. Base Lightning Components are the on-platform implementations of that.
  • “Why do all STYLE classnames start with .THIS?”
    Because .THIS gets substituted later with the name of your component when the stylesheet is compiled, e.g. cTestContactList1. Salesforce does this because two components might have a classname called “.results” and we have to isolate the CSS styles.

Amy Lee

Written by

Amy Lee

I design and program things for the web. I'm a technologist at Salesforce and my opinions may not reflect those of my employer.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade