People working on laptops

React and Angular Apps as Lightning Components

by Nikita Verkhoshintcev

Update 10.11.2019

Here are a few updates to this article that I would like to share first.

  • It's not possible to execute web service methods from the container application by default anymore as it violates the security settings. It could be overwritten but not recommended.

  • If you want to invoke remote actions from the container application, you need to include a manifest.json file with the controller name. It supports only one controller and requires a namespace for managed packages.

    // manifest.json { landing-pages: [ { path: "index.html", apex-controller: "Namespace.ControllerName" } ] }

Thanks!

 

 

Recently, I have been giving a talk on the local Salesforce Developers group meetup concerning the React and Angular applications, why and how to use them in Salesforce Lightning.

Apparently, the more and more companies become interested in using the third party frameworks for Salesforce development to reduce their costs. That is why I also decided to share my thoughts on the blog.

For the past couple of years, I have been working a lot with Salesforce by building the React and Angular applications on this platform.

My end goal was and stays the same, improving the enterprise user experience by developing the highly interactive, performant, and reliable web applications. Therefore, in this article, I am going to show you how JavaScript can help us to achieve this goal.

I will give a quick overview of the following things:

  1. Why use heavy JavaScript
  2. What is its current state
  3. How to build Angular/React apps as a lightning component and communicate data with Salesforce

Why use heavy JavaScript?

Let me start with a couple of examples.

In the first one, we had to develop a sales reports validation system to improve the business process and reduce the error rate.

Furthermore, each report might have thousands of lines, but usually, there is only one person who validates them and creates an invoice.

Eventually, at Flowhaven, we created an Angular app which allows easy navigation, sorting, and error highlighting. We implemented a pre-validation feature that can parse and validate the report against column names even before customer submitted it. All the processing and changes appear to a local app state. For instance, select all lines or performing a mass search & replace operation. As a result, we insert changes to Salesforce in a single action.

Another example, Flowhaven had to build a highly interactive design approval tool to allow a simple collaboration between the parties. It has multiple integrations with external data sources, e.g., users can store files on GoogleDrive or Dropbox.

The user can view images, leave comments on them, upload new versions and compare them.

Currently, we are also working on video files support and drawing functionality.

What combines these two cases? What are the advantages of using heavy JavaScript?

  1. Interactivity and performance. JavaScript can redraw any part of UI without roundtrips to a server to retrieve HTML. Most of the static resources are loaded once and are cached. JavaScript provides unlimited capabilities for a custom data visualization.
  2. Easier application state tracking and management. There is no need to utilize cookies, local and session storage to remember the state between pages.
  3. Scalability. It doesn’t matter how many users we have at the moment if we do most of the processing in the browser.
  4. Easier integrations.
  5. You can surpass the Salesforce limitations.
  6. Enhanced developer experience. Faster development cycle and time-to-market.

The current state of JavaScript

For those who are new to JS, I would like to talk a bit about the current state of the JavaScript.

First and foremost, it improved a lot for the past couple of years, and I genuinely enjoy it.

We have a new version of the programming language. Currently, it is EcmaScript2017 (ES8). Starting from 2015, ES6 introduced the modules, constants, arrow functions, spread operators, deconstructors, class interface, string templating, a lot of stuff.

We have a TypeScript which is a superset of JavaScript. It has optional static typing and, the most important, tooling (IntelliSense) support. That makes it a decent chose for the large-scale projects.

We have a Webpack which is a JavaScript bundler. It allows you to bundle all the modules in static assets and optimize them for production use.

We have Redux. It is a predictable, immutable, meaning that it has no side effects, state container for JS applications. We use it as a single source of truth of the app, and it becomes easy to manage and test the application state.

What about the available frameworks?

Frameworks

The most prevalent these days are Angular and React. Last year there were a lot of discussions, and many argued about which one is better.

It might be a surprise for someone, but I don’t see a big difference between these two from the developer perspective.

Both of them are using the component architecture. Everything is a component, and you need to structure your app accordingly. Even more, the Salesforce lightning framework also inherited this approach.

Both can use the Redux, stateful and stateless components, which is more a concept.

React uses ES6 while Angular forces you to use TypeScript. As far as I know, many companies, for instance, Onninen, use TypeScript for React as well.

React uses functional programming paradigm, where Angular uses functional-reactive.

The most significant difference is that Angular is opinionated when React is unopinionated. Meaning that there is one, right, an Angular way of doing things which you have to follow, but in React you have to make own decisions.

React might be easier to learn for new developers. On the other hand, it’s also easier to make an architecture mistake.

I, personally, follow the simple principle:

Go with the framework which is more familiar to your team.

In our case that was Angular. We build applications on Salesforce platform and our engineers are working with Java and Apex, so Angular with its model and TypeScript looks more appealing to them, and they can better understand the code.

Also, I like that Angular has own dependency injection system, which is useful for large-scale apps.

Tools

In the meantime, I suggest checking out the great tools such as Chrome extensions for Redux, React, and Angular so that you can overview the app in a visual format and debug it with ease.

Also Angular has own command line tool which allows you to create, build and test app and its parts with the CLI commands.

Demo

Let’s get our hands dirty and take a look at our demo application.

We are going to build a simple web app and use it as a lightning component inside the lightning page app. We will display the list of all contacts on one page and user would be able to click on any contact to get more descriptive information.

Before we start, here you can find the source code for this demo app.

What would we need in general?

  1. Lightning application (page)
  2. Apex class
  3. Static resource to store our JavaScript application
  4. Either Lightning component or Visualforce page to load the JS app

We can use lightning components inside lightning apps. Nevertheless, we also could use Visualforce pages. That is why I would like to start with the simplest option.

Visualforce

First of all, we need to create a lightning app either via the app builder or programmatically. Then create a Visualforce page and make it available for lightning by checking the “Available for Lightning Experience, Lightning Communities, and the mobile app.”

When building a third-party framework web application in Salesforce, there are three ways to communicate with Apex:

  1. Remote Actions
  2. Web Services
  3. REST API

For our application we want to have two methods, one will send the list of all contacts, and another will give the detailed information for a selected one.

We would need to create an Apex class to hold the required methods.

Let’s name it AngularPOC.

I would like to start off with the Remote Actions. To do so, we should annotate our methods accordingly, with a @RemoteAction.

// AngularPOC.cls
public class AngularPOC {
  @RemoteAction
  public static List getContacts() {
    return [SELECT Id, Name FROM Contact];
  }
 
  @RemoteAction
  public static Contact getContact(String contactId) {
    return [SELECT Id, Name, Title, Phone, Email FROM Contact WHERE Id =: contactId];
  }
}

To make them invokable, we need to implement the controller on the Visualforce page.

// AngularPOC.html
<apex:page standardStylesheets="false" sidebar="false" showHeader="false" 
  applyBodyTag="false" applyHtmlTag="false" docType="html-5.0"
  controller="AngularPOC">
  <html>
    <head>
      <meta charset="utf-8" />
      <title>Meetup</title>

      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <link rel="icon" type="image/x-icon" href="{!$Resource.AngularPOC + '/favicon.ico'}" />
      <link rel="stylesheet" type="text/css" href="{!$Resource.AngularPOC +  '/assets/main.css'}" />
    </head>
    <body>
      <app-root>Loading...</app-root>
      <script src="{!$Resource.AngularPOC + '/inline.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/polyfills.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/vendor.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/main.bundle.js'}"></script>
    </body>
  </html>
</apex:page>

That will give us access to a global JavaScript variable called Visualforce, which we can use to execute actions from the implemented class.

Here is an example of how can we use it inside Angular application.

// contacts.component.ts
import { Component, OnInit } from '@angular/core';
 
declare const Visualforce;
 
interface Contact {
  Id: string;
  Name: string;
}
 
@Component({
  selector: 'app-contacts',
  templateUrl: './contacts.component.html'
})
export class ContactsComponent implements OnInit {
  contacts: Contact[];
 
  ngOnInit() {
    Visualforce.remoting.Manager.invokeAction(
      'AngularPOC.getContacts',
      (result: Contact[], event) => {
        if (event.status) {
          this.contacts = result;
        } else {
          console.error(event.message);
        }
      },
      { escape: false }
    );
  }
}

If you are using the TypeScript, you should first declare the global variable.

Web Services

The second way to communicate data to Salesforce is using Web Services.

There are two main differences, all the web services are available globally, meaning that we can use any web service method from any class, plus they are synchronous.

Firstly, we need to update the Apex class by making it global and annotating the web service modifier.

Web services send data as is and we have to convert it to the JSON format manually.

// AngularPOC.cls
global class AngularPOC {
  @RemoteAction
  webservice static String getContacts() {
    return JSON.serialize([SELECT Id, Name FROM Contact]);
  }
 
  @RemoteAction
  webservice static String getContact(String contactId) {
    return JSON.serialize([SELECT Id, Name, Title, Phone, Email FROM Contact WHERE Id =: contactId][0]);
  }
}

Then we need to remove the controller from the Visualforce page and import SOAP libraries provided by Salesforce. When we include these libs, we get access to a sforce global variable.

To use the SOAP API, we need to pass a Salesforce access token along. So, we have to assign it to the sforce interface.

// AngularPOC.html
<apex:page standardStylesheets="false" sidebar="false" showHeader="false" 
  applyBodyTag="false" applyHtmlTag="false" docType="html-5.0">
  <html>
    <head>
      <meta charset="utf-8" />
      <title>Meetup</title>

      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <link rel="icon" type="image/x-icon" href="{!$Resource.AngularPOC + '/favicon.ico'}" />
      <link rel="stylesheet" type="text/css" href="{!$Resource.AngularPOC +  '/assets/main.css'}" />
    </head>
    <body>
      <app-root>Loading...</app-root>
      <script src="/soap/ajax/21.0/connection.js" />
      <script src="/soap/ajax/21.0/apex.js" />
      <script>
        sforce.connection.sessionId = '{!$Api.Session_ID}';
      </script>
      <script src="{!$Resource.AngularPOC + '/inline.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/polyfills.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/vendor.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/main.bundle.js'}"></script>
    </body>
  </html>
</apex:page>

Then we can use this var to make a call.

Web service call is synchronous request via the SOAP API.

Since the call is synchronous, we want to use the try-catch block to handle errors. Although to make the call asynchronous we can use new async-await syntax introduced in ES2017.

// contacts.component.ts
import { Component, OnInit } from '@angular/core';
 
declare const sforce;
 
interface Contact {
  Id: string;
  Name: string;
}
 
@Component({
  selector: 'app-contacts',
  templateUrl: './contacts.component.html'
})
export class ContactsComponent implements OnInit {
  contacts: Contact[];
 
  ngOnInit() {
    try {
      this.contacts = JSON.parse(sforce.apex.execute('AngularPOC', 'getContacts', { }));
    } catch (e) {
      console.error(e);
    }
  }
}

// Example of asynchronous web service
// Function that returns a promise with a list of contacts
// and throws if there is any error in Apex. As alternative,
// you can return empty array right there.
async getContacts(): Promise<Contact[]> {
  try {
    const contacts = await JSON.parse(sforce.apex.execute('AngularPOC', 'getContacts', { }));
    return contacts;
  } catch (e) {
    throw new Error(e);
  }
}

this.getContacts()
  .then(contacts => this.contacts = contacts)
  .catch(error => this.contacts = []);
// By the way, Angular has an async pipe so that you can
// display promises directly in the template.

REST API

The third way is using the REST API.

REST is the most convenient way for the React and Angular applications to retrieve data from the server.

The good thing is that we don’t have to rely on Salesforce abstractions.

With this approach, we can use native JavaScript XMLHttpRequest or fetch, Angular’s Http or HttpClient modules or any other library.

Nevertheless, it has own drawbacks. For instance, sometimes in lightning experience, Salesforce redirects requests and changes the POST method to GET.

There are also few additional notes:

  1. You might need to whitelist your domain in the CORS settings
  2. You need to register a custom endpoint via the @RestResource annotation
  3. Each Rest Resource can have only one method of each type: HttpGet, HttpPost, HttpPut, HttpPatch, and HttpDelete.

In this example, I will use one HttpGet method for both contacts list and contact info.

If there is a contact Id parameter, we will return its information, otherwise, return the list of available contacts.

// AngularPOC.cls
@RestResource(urlMapping = '/angularpoc')
global class AngularPOC {
  @HttpGet
  global static List<Contact> getContacts() {
    RestRequest req = RestContext.request;
    String contactId = req.params.get('contactId');
    
    if (contactId != null) {
      return [SELECT Id, Name, Title, Phone, Email FROM Contact WHERE Id =: contactId];
    }
    
    return [SELECT Id, Name FROM Contact];
  }
}

Inside the Visualforce page, we no longer need to load the SOAP libs but we still need to provide the access token which we will send with every request.

// AngularPOC.html
<apex:page standardStylesheets="false" sidebar="false" showHeader="false" 
  applyBodyTag="false" applyHtmlTag="false" docType="html-5.0">
  <html>
    <head>
      <meta charset="utf-8" />
      <title>Meetup</title>

      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <link rel="icon" type="image/x-icon" href="{!$Resource.AngularPOC + '/favicon.ico'}" />
      <link rel="stylesheet" type="text/css" href="{!$Resource.AngularPOC +  '/assets/main.css'}" />
    </head>
    <body>
      <app-root>Loading...</app-root>
      <script>
        var sessionId = '{!$Api.Session_ID}';
      </script>
      <script src="{!$Resource.AngularPOC + '/inline.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/polyfills.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/vendor.bundle.js'}"></script>
      <script src="{!$Resource.AngularPOC + '/main.bundle.js'}"></script>
    </body>
  </html>
</apex:page>

Then all we need is to make the Http request and set access token in Headers. Here is an example of using the Angular’s HttpClient service.

// apex.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
 
declare const sessionId;
 
interface Contact {
  Id: string;
  Name: string;
  Title: string;
  Phone: string;
  Email: string;
}
 
@Injectable()
export class ApexService {
  headers = new HttpHeaders({ 'Authorization': 'Bearer ' + sessionId });
 
  constructor(
    private http: HttpClient
  ) { }
 
  get(contactId?: string): Observable<Contact[]> {
    const params = new HttpParams().set('contactId', contactId);
 
    return this.http.get<Contact[]>('/services/apexrest/angularpoc', {
      headers: this.headers,
      params: contactId ? params : null
    });
  }
}

// contacts.component.ts
import { Component, OnInit } from '@angular/core';
import { ApexService } from '../apex.service';
 
interface Contact {
  Id: string;
  Name: string;
}
 
@Component({
  selector: 'app-contacts',
  templateUrl: './contacts.component.html'
})
export class ContactsComponent implements OnInit {
  contacts: Contact[];
 
  constructor(
    private apex: ApexService
  ) {}
 
  ngOnInit() {
    this.apex.get().subscribe(
      contacts => this.contacts = contacts,
      error => console.log(error)
    );
  }
}

Lightning Container

So far, we’ve learned how to use the Visualforce page as an iframe inside the lighting application, but there is an alternative.

Winter ’18 release brought us a new way to use third-party frameworks inside the lightning experience. It is called lightning container.

It is an interface which allows you to load a third-party web application from the static resource inside the iframe element and provides cross-domain access.

In the next release Salesforce is planning to block REST calls from the lightning containers. However, it is not clear what that would mean in practice.

Technically, all you need to do is to build the application, archive the distribution directory, upload it to the static resource and specify an index.html file as a source of the lightning container.

Utilizing the SFDX and building scripts gives you an option to automate the whole process to improve the development process.

Data Communication

The lightning container provides a new way of data communication between custom app and component besides the previously mentioned options.

It is possible to pass the data back and forth via the message event.

I won’t touch this method in the article because it is not the favorable way of data transfer for several reasons.

  1. You have to pass data twice. First, from the Apex to component and then from lightning component to container.
  2. Not developer-friendly API. It is harder to implement compared to other options.
  3. It fails silently, so it’s harder to do debugging.

Anyway, if you want to try this, you can find the source code for that approach in step/4 branch of the demo repository.

Also, recently I noticed the problem with the interface for the Remote Actions. The Visualforce variable is no longer accessible. So, I didn’t include that method in the article either.

So, at the moment, we have two options, either web services or REST.

Web Services

Let’s see how can we use the web services inside the lightning container.

First of all, instead of a Visualforce page, we need to create a Lighting Component and reference the index.html file.

// AngularPOC.html
<aura:component implements="flexipage:availableForAllPageTypes" access="global">
  <lightning:container src="{!$Resource.AngularPOC + '/index.html'}" />
</aura:component>

Apex controller will be the same as in a Visualforce example.

// AngularPOC.cls
global class AngularPOC {
  @RemoteAction
  webservice static String getContacts() {
    return JSON.serialize([SELECT Id, Name FROM Contact]);
  }
 
  @RemoteAction
  webservice static String getContact(String contactId) {
    return JSON.serialize([SELECT Id, Name, Title, Phone, Email FROM Contact WHERE Id =: contactId][0]);
  }
}

Then we need to include the SOAP libraries in the index.html file. We do not longer need the Visualforce page anymore, just a plain HTML.

// index.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Meetup</title>

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="assets/main.css">
</head>
<body>
  <app-root>Loading...</app-root>
  <script src="/soap/ajax/21.0/connection.js"></script>
  <script src="/soap/ajax/21.0/apex.js"></script>
</body>
</html>

There is a lightning container wrapper available as an NPM package to make things easier.

Install it by running npm i lightning-container.

Now we can import this package into our app and use its interface to get the session Id because we no longer can use Visualforce expressions.

// contacts.component.ts
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import * as LCC from 'lightning-container';
 
declare const sforce;
 
interface Contact {
  Id: string;
  Name: string;
}
 
@Component({
  selector: 'app-contacts',
  templateUrl: './contacts.component.html'
})
export class ContactsComponent implements OnInit {
  contacts: Contact[];
 
  constructor(
    private cd: ChangeDetectorRef
  ) {}
 
  ngOnInit() {
    sforce.connection.sessionId = LCC.getRESTAPISessionKey();
 
    try {
      this.contacts = JSON.parse(sforce.apex.execute('AngularPOC',
        'getContacts', {}));
      /**
      * Angular cannot detect changes made by lightning container by itself,
      * so we have to force the update manually.
      */
      this.cd.detectChanges();
    } catch (e) {
      console.error(e);
    }
  }
}

If you are using web service call inside the Angular application without Redux the change detector won't get the notification about changes. So, you would need to call the detect changes method after the call to explicitly re-render the UI.

Using the REST API services is entirely identical the example from the Visualforce. The only difference is that instead of passing session Id via the global variable you can get it inside application via the lightning container package.

Conclusion

Indeed, using Angular and React applications for Salesforce helps companies to reduce development costs, improve performance, bypass the platform limitations, etc.

In this article, I showed how you could build a third-party JavaScript application. How to communicate data between your application and Apex.

In conclusion, there are three most convenient ways to do it: Remote Actions, Web Services, and REST API.

Web Services might not be the best options because it is using the synchronous call which is affecting the user experience. Plus, you have to use try-catch blocks to handle server errors and stringify the data.

Personally, I suggest using the REST API approach because it’s a standard for modern JavaScript web applications, it is more flexible and gives you more control. Furthermore, Angular has own module to handle these requests in a convenient way.

Call to action

Afterward, if you want to know how to improve the user experience, performance, surpass the limitations and reduce costs of your Salesforce and SAP applications, contact me for a free consultation.

E-mail: nikita@digitalflask.com

Phone: +358 41 471 6220

Nikita Verkhoshintcev photo

Nikita Verkhoshintcev

Salesforce Consultant

Senior Salesforce and full-stack web developer. I design and build modern Salesforce, React, and Angular applications for enterprises. Usually, companies hire me when complex implementation, custom development and UI is required.

Let's work together!

Contact us today and take your digital end-user experience to the next level.

Contact us