Creating Modern Applications on Liferay with ReactJS, Docker, DXP Cloud, and Headless APIs (III: The Finale)

Our last part of our tri-part series on React and Liferay! We finally learn how to take our React applications and convert them into Liferay Workspace ready portlets. 

Hello again! I finished my muffin and rehydrated accordingly. I hope you used your break wisely, as breaks are essential to productivity.

Now, back to work. We have a functioning React application using Headless and Clay, but it runs separately from Liferay. How can we make and deploy a User facing portlet out of our ReactJS application on an instance of DXPC?  

Note: This does not cover how to make a Control Panel JS portlet. That can be covered in another post. This series only covers the basics of setting up a standalone React for local development, and then turning it into a User portlet/widget under catergory.sample.


Firstly, from this point onward, everything that I describe will occur within the directory of a DXP Cloud project. Our cloud hosted project (with Jenkins supports, gradle builds and all) base files were provided by the DXP Cloud team, so if you have experience with knowing how to run a Cloud instance (i.e. blade gw startDXPC, blade gw clean), it is helpful at this stage. This is how a base-level DXP Cloud project would be structured: 

Take note of the modules folder. That is where we are going to store our ReactJS to sample portlets and services.

Standalone React to Liferay Migration


An immediate first step that can be taken is to generate your files for a JavaScript-based Liferay portlet. To do this, an easy option is to the use Yeoman. Yeoman, in its own terms, is an ecosystem generator. It basically creates the files you need for projects you define, in this case being a JS portlet. 

First, download yo via npm and then download the yo liferay js generator:

npm install -g yo 
npm install -g generator-liferay-js 

Once you have those downloaded, navigate to the modules folder as mentioned before in your DXP Cloud project directory and launch the yo command to create a ReactJS widget:

yo liferay-js 
? What type of project do you want to create? (Use arrow keys)
  -- Widget projects --
❯ JavaScript Widget
  Angular Widget
 React Widget   Vue.js Widget
  Metal.js Widget

(Move up and down to reveal more choices)

Go through the prompts the Yeoman triggers, like choosing the local installation directory, widget name, etc.

For the local installation directory, since we are using the Cloud, we do not need to point to a local instance of Liferay. By the end, you should have a file structure like this in the new folder:

.

├── README.md
├── assets                      Can store static images here that portlet/widget can call
├── features                    Localization files (language keys) here 
├── node_modules 
├── package-lock.json
├── package.json                Where we will download our dependencies
└── src                         Where our components will go 

Note that this is a similar layout to what we already have in our pre-existing standalone app. From now on, I will refer to this portlet directory as yo-reservation. All file changes will now be made from here.  

Note that there is a package.json. You can go ahead and copy the ClayUI dependencies from the past package.json on the standalone ReactJS to the dependencies in yo-reservation/package.json and run a quick npm install. (We will go over this again below).

Next thing we can do is migrate the JS component files from frontend-reservation (our standalone React app) to the yo-reservation/src folder. 

To do this, relocate to yo-reservation/src and create a new directory called components. From there, copy the components we have (OfficeTest.js) and their scss files and other structures into the components yo-reservation/src folder. So now, the yo-reservation/src folder should look like this:

./yo-reservation/src
├── components
│   └── Offices
│       ├── OfficeTest.js
│       └── styles
│           └── offices.scss
└── index.js

In this case, we created a folder in the components folder called Offices and inside this folder, we created another folder called styles. In the Offices folder, we will have our components, such as OfficeTest.js, and then in styles, we will place the scss files, in this case the office.scss.

With our original files now in the new folder, we have to make a few adjustments. 


1. All references to .scss files will now be .css

For importing styles to portlets on Liferay, anytime you use as .scss file, the liferay bundler that processes JavaScript for JS based portlets compiles all .scss files to .css files. So, for example in our OfficeTest.js, if we wanted to import our style in the styles folder, it would be via 

import './styles/offices.css'

The path is the same, but the extension used in the component file is css instead of scss, even though we still have it saved as a .scss file.


2. Change properties.js keys to Liferay-based libraries 

For using properties.js on portlets in Liferay for Headless API related purposes, we have to use Liferay libraries for things like authentication. Let's look again at the original 

frontend-reservation/properties.js:

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);

We can use this as the base of the new portlet properties.js file.

Redirect to yo-reservation/src and there, create a properties.js folder. Copy the contents of the original file to start with. 

First, we can go ahead and remove the import icons. We will be importing it via a new line in the properties constant.

Note: For icons.svg and other static media like images in a JS portlet, they should be stored in the pre-created  assets/media folder. These static resources will automatically be processed by the Liferay npm bundler and can be referenced via path '/lexicon/name_of_media'.

Next, for our endpointPaths constant. All of the constants will remain the same, but for our Liferay Workspace, we have an altered endpoint we can use: the Liferay users endpoint. This endpoint can be used to retrieve users on the system. Use if you'd like/need.

  usersEndpoint: "/api/jsonws/user/get-company-users/company-id/"+ properties.companyId +
  "/start/-1/end/-1",

Now, for the properties constant. This is the constant that will have the most changes.

Looking at the original code:

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


For the properties.js in our converted ReactJS to portlet application, we will instead use Liferay libraries for these values in our yo-reservation/src/properties.js file:

export const properties = {
  queryTimeout: 500,
  icons: Liferay.ThemeDisplay.getPathThemeImages() + "/lexicon/icons.svg",
  authToken: Liferay.authToken,
  companyId: Liferay.ThemeDisplay.getCompanyId(),
  portalURL: Liferay.ThemeDisplay.getPortalURL()
};

As we can see, as mentioned before, icons is now fetched via Liferay.ThemeDisplay.getPathThemeImages(), which is a Theme based Liferay library, pointing to the compiled path of our icons.svg file in our assets/media folder. 

For our authToken, we no longer use the aforementioned 'secret' ReactJS default token. We need to use a Liferay oAuth token since we are now trying to host this React application in lieu with Liferay.  So, we can use a Liferay library, which is the Liferay.authToken value. 

For companyId and portalURL, we use again ThemeDisplay to get the values for those.

Everything else, like the for function and Object.assign at the end of the file, will remain the same. The properties.js file of yo-reservation/src should therefore look like this:

export const properties = {
  queryTimeout: 500,
  icons: Liferay.ThemeDisplay.getPathThemeImages() + "/lexicon/icons.svg",
  authToken: Liferay.authToken,
  companyId: Liferay.ThemeDisplay.getCompanyId(),
  portalURL: Liferay.ThemeDisplay.getPortalURL()
};

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: "/api/jsonws/user/get-company-users/company-id/"+ properties.companyId +"/start/-1/end/-1",
};

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

Object.assign(properties, endpointPaths);

So, if all looks good, the only thing you'll have to do is make sure to have the correct imported relative of the properties.js file in any component using these endpoints/APIs. 


3. Changing the index.js and index.html so we can render to the portlet divs correctly 

Remember when I mentioned we would need to remember how we altered index.html and index.js so we could render elements to nodes on the DOM (i.e. root, content). For portlet conversion, we need to do things a bit different in index.js. 

So, firstly, in yo-reservation, there isn't a default index.html, so you won't have to worry about adding divs so we can render components to it (even though that's pretty easy). Instead, we add DOM's to the portlet itself in the index.js file. 

Open up yo-reservation/src/index.js. It will be empty. In the default main function, that takes three parameters, post the contents in the main function from below into it: 

export default function main({portletNamespace, contextPath, portletElementId}) {
const header_id = portletElementId + "-header";
const content_id = portletElementId + "-content";

ReactDOM.render(
<div>
  <div id={header_id}>
  </div>

  <div id={content_id}>
    <OfficeTest />
  </div>
</div>
, document.getElementById(portletElementId));
}

To understand this, we can go one by one. First, we create a string that creates the id names for the nodes we want to add to the inside of our portlet. Then, we render the div's to the portlet itself via rendering to the portletElementId, which is a predefined parameter from Liferay. In this case, we have a header and content id. Header and content id's (i.e. two separate dom nodes) are good for if you are using inner portlet navigation with React, so you can render the navigational component to the header, and navigational-dependent content to the content div. For this case, we just have our OfficeTest component that is being viewed, so we don't need any kind of navigation. We can just render it straight to the content div.


4. Changing the file contents of package.json to include Babel 7 supports and Liferay bundler support

This is pretty important. In order to use latest ReactJS syntax, like Hooks, you will need Babel 7 to compile JS to Java for portlet migration. Another blog post will go over this explicilty. 

So, a large part of the headache comes in the form of Liferay bundler issues and Babel when migrating a React application to portlet. The services that are used in this process, like the liferay-npmbundler, do not necessarily support the latest stack we had used during this project development, so we had to do quite a few things in order to get it to work. This part of the series is very important, almost the most important part. MJ (another consultant on the APAC Japan team) will be covering this aspect more indepth as well (link here when published). 

So, when you create a new project based on the yo liferay-js React Widget build, you will get a few things in your package.json and also a few hidden files (detectable in Terminal with ls -a), like the .npmbundlerrc file. 

Let's tackle the package.json. Since this is a new portlet directory, it will be empty. Let's migrate all of the ClayUI dependencies into the dependencies section of this new yo-reservation/package.json file. You can go ahead and copy the pre-pasted ClayUI dependencies from Part II.

Now, we will need to add new things. To compile things from JS to Liferay-supported Java, we will need

Babel 7https://www.npmjs.com/package/@babel/cli 
Liferay Npm Bundler supportshttps://www.npmjs.com/package/liferay-npm-bundler
Webpackhttps://www.npmjs.com/package/copy-webpack-plugin
Liferay Npm Build supportshttps://www.npmjs.com/package/liferay-npm-build-support

You could definitely in the terminal npm install -g these dependencies, but I think it's easier to copy paste the package.json devDependencies I give you instead, and just delete node_modules, package-lock.json and re-run npm install with these new additions below to your yo-reservation/package.json: 

  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.9.6",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/preset-env": "^7.9.6",
    "@babel/preset-react": "^7.9.4",
    "copy-webpack-plugin": "4.6.0",
    "liferay-npm-build-support": "2.18.4",
    "liferay-npm-bundler": "2.18.4",
    "liferay-npm-bundler-loader-copy-loader": "2.18.4",
    "liferay-npm-bundler-loader-css-loader": "2.18.4",
    "liferay-npm-bundler-loader-json-loader": "2.18.4",
    "liferay-npm-bundler-loader-sass-loader": "2.18.4",
    "liferay-npm-bundler-loader-style-loader": "2.18.4",
    "node-sass": "^4.14.1",
    "sass": "^1.22.9",
    "webpack": "4.29.6",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  }

Again, to recap, these are the dependencies we will need to compile JS to Java for our ReactJS to User portlet. Go ahead and npm install these dependencies. 


5. Changing the file contents of .npmbundlerrc and index.js/App.js for Babel support 

One of the files that is included on Yeoman generation and is necessary for Liferay bundling is the .npmbundlerrc file (if you can't see it in your yo-reservation directory, a simple ls -a command will show all files pre-fixed with a period). The default .npmbundlerrc file contains only one clause: 

{
  "create-jar": {
    "output-dir": "dist",
    "features": {
      "js-extender": true,
      "web-context": "/yo-reservation",
      "localization": "features/localization/Language"
    }
  },
"dump-report": true
}

Which lays out the function to create a jar for the portlet for deployment on the Liferay workspace. For our portlet migration, we are going to need to add a few extra lines in a new sections called rules: [].

In this new rules section, we will add three values, which load and compile sass files, json files and add assets. 

  "rules": [
    {
      "test": "\\.json$",
      "exclude": "node_modules",
      "use": ["json-loader"]
    },
    {
      "test": "\\.scss$",
      "exclude": "node_modules",
      "use": ["sass-loader", "css-loader"]
    },
    {
      "test": "^assets/",
      "exclude": "node_modules",
      "use": ["copy-loader"]
    }
  ]

All these new rules use Liferay loaders and bundlers. So, your yo-reservation/.npmbundlerrc should now look like this:

Another thing you might encounter is the problems with Babel polyfill. A potential fix for that is noted in the index.js/App.js file. By default in these files, you might see the line:

"import babel-polyfill"

To work with our new dependencies, replace that line with 

import "core-js/stable";
import "regenerator-runtime/runtime";

Troubleshooting changes like these are also further explored in MJ's blog post here. Thanks MJ!


Deploying your portlet 
 

Now that we have changed around our files, included the new dependencies and created a portlet directory with our component files from the standalone app, if all is well, all that's next to do is deploy your portlet. 

Since we are building off of the cloud,  the easiest way to build out portlet alongside the natural build for the workspace is by using the command:

blade gw startDXPC

When using this command to build you DXP Cloud instance, it also builds every module in the modules folder. If it has successfully compiled the portlet (yo-reservation), you can find it on the workspace under the categorization determined from before on creating your widget through Yeoman (most probably under category.sample):



Wrapping Up


So, I hope you guys learned a thing or two from this blog series; I know I did throughout the whole process.  With these snippets in mind, the next time you create a ReactJS application and try to migrate it to Liferay, it'll be a piece of cake! 

Please feel free to ask any questions or leave comments down below. Sayonara for now.

Alexa.

2
Blogs

could you share/link the code for the react app  frontend-reservation?