Creating Modern Applications on Liferay with ReactJS, Docker, DXP Cloud, and Headless APIs (II)

Part II of our 3-part series on building ReactJS applications in Liferay Workspaces! 

Welcome back!

What did you do during our intermission? I went to the grocery store to get some lunch. I got a bento box with an assortment of pickled and marinated things on top of brown rice. They skimped me out on the protein though; I only got two slices of pork (which was, by the way, delicious).

Anyways, on with the tutorial!


Working With Cloud Hosted API’s in ReactJS Liferay Applications 


As said last time, the next part of our series will go over how we used API's to present and manipulate data in our ReactJS application.

Let's reference back to our OfficeTable:

In the prior blog post, an actual OfficeTable should be populated with data that is stored on the backend; existing offices in our hypothetical system. So, in order to deal with real data, we will use our headless API we created for this project.

Let's look at an example of a working OfficeTable that uses fetch calls to the backend for rendering data:

 The format is the same, but this time we're using a separate function and API call in order to store JSON data as states to render our component alongside. 

"A whatty what wha" you might be thinking. More pictures should help! Let's take a look into parts of our OfficeTest.js again.

.
.
.
export default class OfficeTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      rows: 5
    }
    this.createTestRows = this.createTestRows.bind(this);
  }

createTestRows(noRows) {
  const rows = []
  for(var i = 0; i < this.state.rows; i++) {
    rows.push(
      <ClayTable.Row>
        <ClayTable.Cell align={'left'}>Test</ClayTable.Cell>
        <ClayTable.Cell align={'left'}>Test</ClayTable.Cell>
        <ClayTable.Cell align={'right'}>
          <ClayButtonWithIcon
            symbol="trash"
            spritemap={icons}
            displayType={'unstyled'}
            style={{padding: '0', margin: '0', height: '1rem'}}/>
        </ClayTable.Cell>
      </ClayTable.Row>
    )
  }
  return rows;
}

In this part of the file, we can see we are rendering a static amount of 5 rows in the function createTestRows as predefined ClayTable. Cells with values of Test and a Trash icon. We then render this function in the return of the class file, showcasing our Test data table on index.js

For this example of using Headless API's, we will transform this Test data into real data via fetch call from backends and link a delete call to the Trash icon.


Introducing: The API's


So, in order to utilize the API's that we have, we must understand how they work. For this project, MJ Sepe created custom API's hosted on the DXP Cloud instance of our project. The system in question deals with creating bookings and reservations in office rooms dependent on availability/time, facilties, purposes, location, and more. 

In this case, let's look at the Office API example he created. You can view and edit API's in Swaggerhub,  and test API results in Postman. Let's look at the structure of the Office API GET call in Swaggerhub, and it's schema. 

Viewing the Office get call on Swaggerhub. 

Office:
  required:
    - location
    - name
    type: object
    properties:
      location:
        type: string
        description: Office's location or address.
      name:
        type: string
        description: Office's name.
      officeId:
        type: integer
        description: The primary key for the Office entity.
        format: int64
        readOnly: true
    xml:
      name: Office

And viewing the defined schema. Is also included within the YAML, but placed separately to better understand.

The request to fetch Office data points is called through a URL. For this case, we want to fetch all of the Office data on the back-end to display on the front-end. I could get into the definition of Headless and how API's really work, but quite frankly even after 6 months of working with API's, it still remains an endless pit of "Don't know what's going on, but it's definitely working". 

For fetching the batch of Office cells, we use the URL http://localhost:8080/o/headless-reservation/v1.0/offices as a GET request. In Postman, you can test the URL directly by posting as follows under a new GET request. Make sure to define the Authentication headers according to your Liferay Workspace where the API services are stored. In most cases, test@liferay parameters should suffice. 


A successful Office GET request.


Defining Authorization parameters to access the API.

Note: If there are still troubles with access, it might be a CORS issue. In order to use hosted service API's available on a local or separate Liferay service, you have to enable Web Content CORS (tutorial here under Enabling CORS for JARS-RX Applications)

On submitting the request, since I have data already on the backend, I can see my data in JSON format. When creating an API, you have to define object keys and value types as you can see in the Swaggerhub body. For an Office object, every Office has at least name, location, and officeId. An example:

{
   "location": "NYC, NY",
   "name": "New York Office",
   "officeId": 201
}

So now that we tested that our API works on Postman, let's migrate it for use on our ReactJS application.


Defining Our API Properties 
 

Taking the URL that we have, let's look at it once more and dissect it. 

http://localhost:8080/o/headless-reservation/v1.0/offices

  • Our service is called headless-reservation 
  • We are calling the Offices endpoint 
  • It is a general call to the Offices endpoint; in the URL itself we are not explicitly saying we want to GET, POST, DELETE, etc.
  • We also are not defining any authorization parameters, even though we will need to inorder to gain access to the API when a request is sent. 

For starters, we want to GET all Offices. What if we want to DELETE or POST? Will we have to, in each file we create, define these parameters, requests, authorizations and links? You definitely could if you wanted to; no judgement here. But, a simpler programmatically minded way to avoid these extra layers of effort (笑) would be by defining a properties.js file. 

Let's look at our properties.js file for our local development:

import icons from './components/svg/icons.svg';

const endpointPaths = {
  roomsEndpoint: "/o/reservation-headless/v1.0/rooms",
  officesEndpoint: "/o/reservation-headless/v1.0/offices",
  purposesEndpoint: "/o/reservation-headless/v1.0/purposes",
  bookingsEndpoint: "/o/reservation-headless/v1.0/bookings",
  amenitiesEndpoint: "/o/reservation-headless/v1.0/amenities",
  participantsEndpoint: "/o/reservation-headless/v1.0/participants",
  usersEndpoint: "/o/headless-admin-user/v1.0/user-accounts",
};

export const properties = {
  queryTimeout: 500,
  icons: icons,
  authToken: "authtokensharedsecret",
  portalURL: "http://localhost:8080"
};

for (var key in endpointPaths) {
endpointPaths[key] = properties.portalURL + endpointPaths[key] + '?p_auth=' + properties.authToken;
}

Object.assign(properties, endpointPaths);

There are many things to note. Let's start from the beginning.

As mentioned in the previous blog post, a common element you will encounter when developing React applications with Clay is the icons.svg. This is a large svg file with all of the path vectors for custom icons you can use in your frontend. We stored the icons.svg file in a separate folder in the components directory, so we are importing it via relative path in the properties.js. 

The endpointPaths constant is a collection of objects with links pointing to the related API endpoints. This make it easy to reuse in component files.

The properties constant has four values:

  1. queryTimeout set to any millisecond value 
  2. icons that should point to the relative icons.svg 
  3. authToken as "authtokensharedsecret"
  4. portalURL as the URL to our local Liferay  

The  authToken variable is similar to the concept of oAuth, which is a token/string value used to grant access to third-party applications, in this case API's. For standalone ReactJS applications, the default secret key is called "secret". We include this in the properties as authtokensharedsecret. This is important, as it's basically what allows us to make calls and communicate via Headless.

The portalURL is the link leading to your Liferay Workspace. Since we are working locally, it will be at the default port on localhost, which is 8080 for Liferay. 

Looking at the next section of code, we see this:

for (var key in endpointPaths)  { 
  endpointPaths[key] = properties.portalURL + endpointPaths[key] + '?p_auth=' + properties.authToken;
} 

Object.assign(properties, endpointPaths);

This builds each endpoint with the appended authToken to call in our JS code. It then rewrites the properties constant to have these correctly build endpoint paths.


Using Our API's in JS Component Files: Fetching, Deleting and Posting 


Now that we have configurations in order, let's go ahead and try to use these paths in a component file. Returning back to our OfficeTest.js, let's try to create a static table that fetches Office data, and also deletes Office data. 

First, we introduce the new properties.js file by importing it on the top, and create a simple initial request constant:

import { properties } from '../../properties.js';

const requestOptions = {
"async": true,
"crossDomain": true,
"method": "GET",
"headers": {
  "cache-control": "no-cache",
}}

From here, we have all of the pieces we need to call an API from a function in the JS component. 

It is best to store the fetched data as a state, which can be used anywhere in a JS file. So, let's create a fetchOffices function that will fire every time the OfficeTest.js component is mounted. 

fetchOffices(url) {
  fetch(url, requestOptions)
  .then((res) => {
    if (!res.ok) throw new Error();
    else return res.json();
  })
  .then((data) => {
    this.setState({
      offices: data.items
    })
  });
}


componentDidMount() {
  this.fetchOffices(properties.officesEndpoint);
}

This is a pretty straightforward way to use Headless that is consistently used across our frontend-reservation app in multiple components. 

We first create a fetchOffices  function that takes in a url parameter. In this case, the URL is the properties.officesEndpoint, which we defined earlier in our properties file as the headless pointer to "/o/reservation-headless/v1.0/office", attached with the oAuth token generate ('secret' for standalone React applications). We then implement a built-in ReactJS call, componentDidMount. 

By calling  fetchOffices within componentDidMount, React will fetch all Office data on the backend anytime we detect our application will render the OfficeTest component

The fetched data comes to us in the form of a JSON Object. Here is an example on Postman:

We see that all of the data is stored as the array value for a key called items. Each Office object has a location, name and unique id (known as officeId). 

If the fetch is successful, the items value is stored as the new value for our offices state in the component. Let's console.log this.state.offices on render to see what we get on a successful call in OfficeTest. 

render() {
  const spritemap = icons;
  console.log(this.state.offices, "Successful backend fetch to the API!");)
  return ( ... )}

The result of the console log above

So we can see that now we have the data stored in a state variable. We can then use this state variable to display all the fetched data on the frontend within our component. Let's take our previous createTestRow function and instead use real data.

Renaming the function to createOfficeRows, it should look something along these lines:

createOfficeRows() {
var body = [];
this.state.offices.map((office, i) => {
  body.push(
    <ClayTable.Row>
      <ClayTable.Cell align={'left'}>{office.name}</ClayTable.Cell>
      <ClayTable.Cell align={'left'}>{office.location}</ClayTable.Cell>
      <ClayTable.Cell align={'right'}>
      <ClayButtonWithIcon
        symbol="pencil"
        spritemap={icons}
        displayType={'unstyled'}
        style={{padding: '0', margin: '0', height: '1rem'}}
       />
     <ClayButtonWithIcon
       symbol="trash"
       spritemap={icons}
       displayType={'unstyled'}
       style={{padding: '0', margin: '0', height: '1rem'}}
     />
     </ClayTable.Cell>
     </ClayTable.Row>
     )
    });
  return body;
}

Now in this new function, we create an empty array to hold our ClayTable Rows. Each row is built off mapped values from the state, and pushed as a value to display on a Table cell. If everything is successful, you can call the function in the return value of the component and get a live-data table just like below:

We can also tie a delete functionality. Let's tie it to the onClick value of our trash icon ClayButtonWithIcon. We will need to add a new type of backend options underneath requestOptions called deleteOptions:

const deleteOptions = {
"async": true,
"crossDomain": true,
"method": "DELETE",
"headers": {
  "cache-control": "no-cache",
}}

The only difference this has from the previous requestOptions is that the method instead of GET is now DELETE. 

We also have to create a separate function:

deleteEntry(name, id) {
var officeUrl = officesEndpoint;
officeUrl = officeUrl.split('?');
officeUrl = officeUrl[0] + '/' + id + '?' + officeUrl[1];

if(window.confirm(name + ": " + "delete?")) {
  fetch(officeUrl, deleteOptions)
  .then(() =>
    this.fetchOffices(this.fetchOffices(properties.officesEndpoint)))
  .catch((err) => {
    console.log(err);
});
}}

For the deleteEntry function, we have to call the endpoint URL on a specific Office via id to delete. So, say we wanted to delete the Morocco Office, which has an id of 101. As a method call to the backend, the request would look something like: DELETE http://localhost:8080/o/reservation-headless/v1.0/offices/101

Of course, the URL would look different for each individual Office. Therefore, the URL should be built for each Office entry, which we link to the ClayButtonWithIcon onClick() feature by adding this line:

onClick={() => this.deleteEntry(office.name, office.officeId)}

Now you will have a working delete function!


Digesting It All


So for this part of the series, we learned how to integrate secure API's into our ReactJS components in order to render live data from the backend into our frontend designs. We dealt with simple GET and DELETE methods, and created a responsive table that reflects real-time data changes.

Now that we have a working example of a React application using Headless API's, the next and final part of this series will finally tackle how to migrate the standalone app to a portlet on Liferay 7.2. 

Which means it's time for a break! I grabbed one of my favorite muffins from a bakery/deli around the corner. It's amazake coconut flavor :-)