Your New Life With Lightning Actions: Smart, Fast, and Mobile

This is the third and final blog in a series on migrating away from JavaScript buttons, and toward solutions that are mobile- and Lightning-friendly.

In our previous two posts, we cited popular use cases for JavaScript buttons, and provided better alternative solutions. However, those solutions don’t address every imaginable use case, and for that we’ve made available a new feature called Lightning Actions in our Winter ’17 release.

You probably already know and love quick actions, but in case you don’t, quick actions are easily accessible custom actions users can do in micro moments throughout the day. Lightning Actions are simply quick actions that call Lightning components.

That’s great news if you’ve already built Lightning Components. Simply add an interface to the component, and zap, you have a Lightning action! If you want an object-specific Lightning action, then just add another interface. Life with Lighting couldn’t be easier!

The rest of this blog post is going to focus in-depth on the development aspects of Lightning Actions. If you haven’t built Lightning components before, you should visit the Lightning Development Center, do the Lightning Component Quick Start Trailhead project, and complete the Lightning Components Basics Trailhead module.

With those prereqs out of the way, let’s dive into Lightning actions by addressing three use cases for JavaScript buttons that can be better solved with Lightning actions:

  • Populate fields based on user input
  • Provide feedback messages to users during data entry
  • 3rd Party API integration

Use Cases #1 & 2: Populate Fields Based on User Input & Provide Feedback Messages to Users

A popular use case is to validate or manipulate data when users are working with records. This lets you give your users feedback or instructions for handling more complicated business processes.

We’ll use an example to illustrate how easily you can validate or manipulate data in a Lightning component before your users create or update a record. We’ve built a custom object called Case Studies. Our sample org uses case studies to track different research projects and adds test users to those records.

When we add new test users, we capture the users’ names and email addresses, which we want to make sure are entered correctly, since that’s our primary contact method. We also want to create a unique nickname so that during the research, test users can remain anonymous.

So we’re going to create a Lightning action that has the name and email address fields, validates the email address, and uses the first name and a random number to automatically create the nickname.

Here’s the code for the component used by this Lightning action.

CreateUser.cmp is the lightning component that you see when you open the action – it contains the UI implementation, which includes the text fields, buttons, action title, etc.
CreateUserController.js is the controller that listens to the component and any UI events that take place, such as the initial load, text input, and button clicks. Its function is to delegate these events to the helper class, CreateUserHelper.js.
CreateUserHelper.js deals with all the business logic, such as validating the password and email fields, and communicating with the server-side Apex controller, which saves the data.
SaveTestUser.apxc is a very simple Apex controller that handles the request to create a new test user.

CreateUser.cmp

<aura:component implements="force:lightningQuickActionWithoutHeader,force:hasRecordId" controller="SaveTestUser" >
   <aura:attribute name="user" type="Test_User__c" default="{ 'sobjectType': 'Test_User__c' }"/>
   <aura:attribute name="hasErrors" type="Boolean" description="Indicate whether there were failures or not" />
   <aura:attribute name="caseStudy" type="String" />
   <aura:attribute name="recordId" type="String"/>

   <aura:handler name="init" value="{!this}" action="{!c.init}" />

   <div class="slds-page-header" role="banner">
      <p class="slds-text-heading--label">Case Study</p>
      <h1 class="slds-page-header__title slds-m-right--small slds-truncate slds-align-left" title="Case Study Title">{!v.caseStudy}</h1>
   </div>
   <br/>

   <aura:if isTrue="{!v.hasErrors}">
      <!-- Load error -->
      <div class="userCreateError">
         <ui:message title="Error" severity="error" closable="true">
            Please review the error messages.
         </ui:message>
      </div>
   </aura:if>

   <div class="slds-form--stacked">

      <div class="slds-form-element">
         <label class="slds-form-element__label" for="firstName">Enter first name: </label>
         <div class="slds-form-element__control">
            <ui:inputText class="slds-input" aura:id="firstName" value="{!v.user.first}" required="true" keydown="{!c.updateNickname}" updateOn="keydown"/>
         </div>
      </div>

      <div class="slds-form-element">
         <label class="slds-form-element__label" for="lastName">Enter last name: </label>
         <div class="slds-form-element__control">
            <ui:inputText class="slds-input" aura:id="lastName" value="{!v.user.last}" required="true" />
         </div>
      </div>

      <div class="slds-form-element">
         <label class="slds-form-element__label" for="nickname">Enter nickname: </label>
         <div class="slds-form-element__control">
            <ui:inputText class="slds-input" aura:id="nickname" value="{!v.user.nickname}" required="false"/>
         </div>
      </div>

      <div class="slds-form-element">
         <label class="slds-form-element__label" for="userEmail">Enter user's email:</label>
         <div class="slds-form-element__control">
            <ui:inputEmail class="slds-input" aura:id="userEmail" value="{!v.user.Email__c}" required="true"/>
         </div>
      </div>

      <div class="slds-form-element">
         <label class="slds-form-element__label" for="userPassword">Enter user's password:</label>
         <div class="slds-form-element__control">
            <ui:inputSecret class="slds-input" aura:id="userPassword" value="{!v.user.Password__c}" required="true"/>
         </div>
      </div>

      <div class="slds-form-element">
         <ui:button class="slds-button slds-button--neutral" press="{!c.cancel}" label="Cancel" />
         <ui:button class="slds-button slds-button--brand" press="{!c.saveUserForm}" label="Save User" />
      </div>
   </div>

</aura:component>

CreateUserController.js

({    
   init : function(component, event, helper) {
      var action = component.get("c.getCaseStudy");
      action.setParams({"recordId": component.get("v.recordId")});
    
      action.setCallback(this, function(response) {
         var state = response.getState();
         if(component.isValid() && state == "SUCCESS"){
             component.set("v.caseStudy", response.getReturnValue());
          } else {
             console.log('There was a problem and the state is: '+state);
          }
      });
      $A.enqueueAction(action);
   },
    
   updateNickname: function(component, event, helper) {
      // Update the nickname field when 'tab' is pressed
      if (event.getParams().keyCode == 9) {
         var nameInput = component.find("firstName");
         var nameValue = nameInput.get("v.value");
         var nickName = component.find("nickname");
         var today = new Date();
         nickName.set("v.value", nameValue + today.valueOf(today));   
      }
   },
 
   saveUserForm : function(component, event, helper) {
      var name = component.get("v.user.first");
      var last = component.get("v.user.last");
      var password = component.get("v.user.Password__c");
      var email = component.get("v.user.Email__c");
      var nickname = component.get("v.user.nickname");
        
      var passwordCmp = component.find("userPassword");
      var emailCmp = component.find("userEmail");
        
      helper.validatePassword(component, event, helper);
      helper.validateEmail(component, event, helper);

      if (passwordCmp.get("v.errors") == null && emailCmp.get("v.errors") == null) {
         component.set("v.hasErrors", false);
         helper.save(component,name + " " + last,password,email,nickname);         
      } else {
         component.set("v.hasErrors", true);
      }
   },
    
   cancel : function(component, event, helper) {
      $A.get("e.force:closeQuickAction").fire();
   }
})

CreateUserHelper.js

({
   save: function(component, name, password, email, nickname) {
      //Save the user and close the panel
      var action = component.get("c.saveUser");
         action.setParams({
            "name": name,
            "password": password,
            "email": email,
            "nickname": nickname,
            "caseStudy": component.get("v.recordId")
         });

      action.setCallback(this, function(a) {
         var response = a.getReturnValue();
         var state = action.getState();
         if(component.isValid() && state == "SUCCESS"){
            $A.get("e.force:closeQuickAction").fire();
            var toastEvent = $A.get("e.force:showToast");
            toastEvent.setParams({
                  "title": "Success!",
               "message": "The test user has been created."
            });
            toastEvent.fire();
            $A.get('e.force:refreshView').fire();
         } else if (state == "ERROR") {
            console.log('There was a problem and the state is: '+ action.getState());
         }
      });
      $A.enqueueAction(action);
      },

   validatePassword : function(component, event, helper) {
      var inputCmp = component.find("userPassword");
      var value = inputCmp.get("v.value");

      if (value == undefined) {
         inputCmp.set("v.errors", [{message: "You must enter a password."}]);
      } else if (value.length < 7 || value.length > 15) {
         inputCmp.set("v.errors", [{message: "The password is the wrong length: " + value}]);
      } else if (value.search(/[0-9]+/) == -1) {
         inputCmp.set("v.errors", [{message: "The password must contain at least one number: " + value}]);
      } else if (value.search(/[a-zA-Z]+/) == -1) {
         inputCmp.set("v.errors", [{message: "The password must contain at least one letter: " + value}]);
      } else {
         inputCmp.set("v.errors", null);
      }
      },

   validateEmail : function(component, event, helper) {
      var inputCmp = component.find("userEmail");
      var value = inputCmp.get("v.value");

      if (value == undefined) {
         inputCmp.set("v.errors", [{message: "You must enter an email."}]);
         return;
      }

      var apos = value.indexOf("@");
      var dotpos = value.lastIndexOf(".");

      if (apos<1||dotpos-apos<2){
         inputCmp.set("v.errors", [{message: "Email is not in the correct format: " + value}]);
      } else if (value.substring(apos+1, dotpos) != "gmail") {
         inputCmp.set("v.errors", [{message: "Email must be a gmail account: " + value.substring(apos+1, dotpos)}]);
      } else {
         inputCmp.set("v.errors", null);
      }
      }

})

 

SaveTestUser.apxc

public class SaveTestUser {
    
    @AuraEnabled 
    public static Test_User__c saveUser(String name, String password, String email, String caseStudy, String nickname) {  
    
        Test_User__c testUser = new Test_User__c(Name=name, Password__c=password, Email__c=email, Nickname__c=nickname, Case_Study__c=caseStudy);

        upsert testUser;
        return testUser;
    }
    
    @AuraEnabled
    public static String getCaseStudy(String recordId) {
        Case_Study__c caseStudyInstance = [SELECT Name FROM Case_Study__c WHERE id=:recordId];
        return caseStudyInstance.Name;
    }

}

After creating the Lightning component, we assign it to an action. In the object management settings for Case Study, we go to Buttons, Links, and Actions, click New Action, then configure the action like this:

Then we add our new Lightning action to the Case Study page layout.  When users invoke it, they see the Lightning action we created.

The great thing about this is that it also works in Salesforce1:

Use Case #3: 3rd-Party API Integration

Another very popular use case is to use JavaScript buttons for integration with 3rd-party systems.

The primary difference between using JavaScript buttons for API integration and doing the same using Lightning actions is that for the latter, you must use a server-side controller. There are many benefits to this approach, including better handling of security credentials, and using Apex for asynchronous and batch API calls.

Let’s say that we’re in the luxury travel business, and we handle celebrities and VIPs. We want our customer service agents to be able to communicate with their clients, but we don’t want to expose the clients’ personal contact information to anyone. We created a Lightning action on the Contact object so that agents can send messages without accessing the contact’s phone number.

To illustrate this, we’ll show you an example of an integration with Twilio for sending SMS messages. As before, we’ll create the Lightning component and helper classes, and then create a quick action to invoke the component.

This Lightning action is comprised of the Lightning component, a JavaScript controller, and an Apex controller which references library classes that handle the Twilio integration.

SendTwilioSMS.cmp

<aura:component controller="TwilioSendSMSController" implements="flexipage:availableForAllPageTypes,force:hasRecordId,force:lightningQuickAction" >
   <aura:attribute name="textMessage" type="String" />
   <aura:attribute name="destinationNumber" type="String" />
   <aura:attribute name="messageError" type="Boolean" />
   <aura:attribute name="recordId" type="String" />

      <aura:handler name="init" value="{!this}" action="{!c.init}" />

   <aura:if isTrue="{!v.messageError}">
      <!-- Load error -->
      <div class="userCreateError">
         <ui:message title="Error" severity="error" closable="true">
            Unable to send message. Please review your data and try again.
         </ui:message>
      </div>
   </aura:if>

   <div class="slds-form--stacked">
      <label class="slds-form-element__label" for="instructMsg">Please enter the message (max 160 char) below: </label>
      <br/>
      <div class="slds-form-element__control">
         <ui:inputText class="slds-input" aura:id="message" label="Text Message" value="{!v.textMessage}" required="true" maxlength="160" size="165" />
      </div>
      <div class="centered">
         <ui:button class="slds-button slds-button--brand" press="{!c.sendMessage}" label="Send Message"/>
      </div>
   </div>
</aura:component>

SendTwilioSmsController.js

({
   init : function(component, event, helper) {
      var action = component.get("c.getPhoneNumber");
      action.setParams({"contactId": component.get("v.recordId")});
      action.setCallback(this, function(response) {
         var state = response.getState();
         if(component.isValid() && state == "SUCCESS"){
            component.set("v.destinationNumber", response.getReturnValue());
         } else {
            component.set("v.messageError", true);
         }
      });
      $A.enqueueAction(action);
   },

      sendMessage : function(component, event, helper) {
      var smsMessage = component.get("v.textMessage");
      var number = component.get("v.destinationNumber");
      var recordId = component.get("v.recordId")

      var action = component.get("c.sendMessages");
      action.setParams({"mobNumber": number, "message": smsMessage, "contactId": component.get("v.recordId")});
      action.setCallback(this, function(response) {
         var state = response.getState();
         if(component.isValid() && state == "SUCCESS"){
            $A.get("e.force:closeQuickAction").fire();
            var toastEvent = $A.get("e.force:showToast");
            toastEvent.setParams({
               "title": "Success!",
               "message": "SMS has been sent woo hoo!"
            });
            toastEvent.fire();
         } else {
            component.set("v.messageError", true);
         }
      });
      $A.enqueueAction(action);
   }
})

SendTwilioSmsController.apxc

/*
* Apex controller that currently contains only one method to send sms message
*/
global class TwilioSendSMSController {

   /*
   * This method uses the Twilio for Salesforce library class and method to
   * send the message using the Twilio api.
   */
   @AuraEnabled
      webService static String sendMessages(String mobNumber, String message, Id contactId) {
         System.debug('the mobNumber is: '+ mobNumber + ' and the message is: '+ message + ' and contactId is: ' + contactId);

         if (mobNumber == null) {
            mobNumber = getPhoneNumber(contactId);
         }

         try {
            TwilioRestClient client = TwilioAPI.getDefaultClient();

            Map<String,String> params = new Map<String,String> {
               'To' => mobNumber,
               'From' => '15555551234',
               'Body' => message
               };
            TwilioSMS sms = client.getAccount().getSMSMessages().create(params);
            return sms.getStatus();
         } catch(exception ex) {
            System.debug('oh no, it failed: '+ex);
            return 'failed';
         }
      }

      @AuraEnabled
      public static String getPhoneNumber(Id contactId) {
         Contact currentRecord = [SELECT Phone FROM Contact WHERE Id = :contactId];
         return currentRecord.Phone.replace(' ', '').replace('-', '').replace(')', '').replace('(', '');
   }
}

As with the prior example, after creating the component, we created a new Lightning action, and this time added it to the Contact page layout so agents can access it.

Again, the cool thing is that the action is also available in Salesforce1, so if a car service provider is trying to contact the customer at the airport, the driver can reach them easily.

Lightning actions are the future of programmatic actions in Lightning Experience. We’ve already had many customers and partners use them as part of their developer orgs, and the response has been very positive. We hope that you will start looking at this set of solutions as better alternatives to JavaScript buttons.

Some of you depend on partner apps that use JavaScript buttons. The good news is that many of our AppExchange partners have already started migrating and upgrading their apps to Lightning, so you should see more apps that are updated for Lightning Experience.

Additional Information

If you’d like to try Twilio for yourself, you can find an unmanaged package of the Twilio for Salesforce library class used in the 3rd Party integration example in the Salesforce Github library: https://github.com/twilio/twilio-salesforce

Before using Twilio for Salesforce and the Twilio API, you need to create a Twilio account, set up a phone number, and connect your account to Salesforce; see the TwilioApi.cls in the above library for more detailed instructions.

For more information on the Lightning Component Framework, check out “Why Use the Lightning Component Framework?” and “Get Started with Lightning Components” in Trailhead.

Leave your comments...

Your New Life With Lightning Actions: Smart, Fast, and Mobile