Blogs
Creating a React portlet is easier than it has ever been.
Introduction
A client asked me to build a React portlet for them and I was like, "Oh Yeah!" I mean, it's been a while since I built a React portlet and tooling has really changed a lot since then, so I was really looking forward to this new task.
So without further ado, I'm just going to dive right in...
Creating the Module
Okay, so this is still Liferay and still OSGi, so we're going to be building a module.
You have basically two options:
1. Use the Yeoman generator-liferay-js generator. This is the
path to take when you are not already using a Liferay Gradle
Workspace. After you install it, use the yo liferay-js
command to start the generator. If this is the route you want to
take, first install yo and the generator using the command npm
install -g yo generator-liferay-js
and then you can use the
yo liferay-js
command.
2. Use Blade to create a liferay-js module. This actually uses
the same Yeoman generator, but it will create your module with the
necessary stuff to allow the Gradle workspace to build your module.
Command to create the module is blade create -t
js-widget my-module-name
. If you have Blade
installed, you don't need anything more.
generator-liferay-bundle
.
This was prevalent at some point (that I think I missed entirely),
but now it is deprecated. Also, in Blade there is the
npm-react-portlet
module type which actually builds a
larger Java portlet around the launch of the React code. This can be
handy if you need something like defined portlet permissions. I feel
like this one is going to be a larger by-hand responsibility for
building the React code than using the standard React tooling, but I
don't know this for certain.Deciding between which of the options to go with comes down to an answer to a simple question: Are you also using a Liferay Gradle Workspace? If yes, go with #2 as you can include your React portlet build in with your other customizations. If no, go with #1 and you can build your project separately.
And if you're a Maven person? Well, um, yeah... For you I'd probably try to do option #1 on its own, if possible. Maven is capable of invoking npm to do a build, though, so it should be possible to use #1 to create a module in your Maven workspace and then add some pom magic to complete the build, but I'm afraid you're on your own there. If someone wants to share a pom in the comments, that would be awesome!
Invoking the liferay-js Generator
Regardless which option you pick, it will always end up running the liferay-js generator.
The generator is going to prompt you a number of times for values, this is what you'll see:
? What type of project do you want to create? React Widget ? What name shall I give to the folder hosting your project? modules/my-module ? What is the human readable description of your project? My React Portlet ? Do you want to add localization support? Yes ? Do you want to add configuration support? Needs Liferay DXP/Portal CE 7.1 with JS Portlet Extender 1.1.0 or Liferay DXP/Portal CE 7.2+. Yes ? Under which category should your widget be listed? category.react ? Do you have a local installation of Liferay for development? Yes ? Where is your local installation of Liferay placed? ~/liferay/react/bundles ? Do you want to generate sample code? Yes
The first prompt is for the JS portlet type. You can pick JavaScript, Angular, React, Vue.js or Metal.js. Obviously I chose React for my portlet.
Next is the folder where your code will be placed. If outside of a workspace, you can pick anything, but in the workspace it will use the modules directory and the my-module name which you specified on the blade command line.
The human readable description is really just your friendly portlet name.
You have the option of enabling Localization for your portlet. Most of the time I opt to support localization.
The next option is to enable configuration support. I was a bit thrown when I saw this prompt the first time because you answer underneath the note, I didn't realize it was connected to the question above. I like configuration options, so I chose yes. The note just tells you the minimum requirements for config support, so you can't use this in 7.0 or 7.1 without the JS portlet extender.
The next prompt is for the category where your portlet will appear in the widgets menu. "category.name" is standard and can then be handled with localization.
Next two questions are about your local dev bundle. If you are not using the workspace, you'll need to pick your path. If you are using the workspace, it assumes you're going to use the workspace bundle, but you can change it if necessary.
Finally, you can generate the sample code in your new portlet. Actually I'd suggest always saying yes to this prompt as it starts your localization and config for you, and basically lays a solid foundation for your portlet without introducing any real cruft.
After this last question, the generator will create your new module folder.
Anatomy of the Liferay JS React Portlet
After the code has been generated, you'll have this structure:
$ tree my-module my-module ├── README.md ├── assets │ └── css │ └── styles.css ├── build.gradle ├── config.json ├── features │ ├── configuration.json │ └── localization │ └── Language.properties ├── package.json └── src ├── AppComponent.js └── index.js 5 directories, 8 files
This is even with the sample data thrown in!
The README.MD is just what you think it is. It starts out being blank, but I love to use this to add documentation about my react portlet. For example, the one I did for the client had some SB remote services thrown in, so in the README I included the code for calling the right services and the kinds of JSON I got back as a result. It can be a handy place to keep notes like this.
The assets folder will include hard assets like css files, images, etc.
build.gradle I got because I'm in a workspace, but it is empty if you check it.
config.json is configuration for the module itself. If you check it you'll find it includes module and build details.
The features folder holds the config and/or localization stuff. Localization is the standard Liferay resource bundle stuff, just know that it will be copied into the right spot at build time to make all of your localization strings work as expected. The configuration.json I'm going to cover in more detail in a coming section.
The package.json is the file every node
developer recognizes. There are some things in here that are
Liferay-specific, including all of the portlet properties we know
from the portlet.xml and liferay-portlet.xml from the past or Java
portlet @Component
annotations from the present. I
wouldn't tamper with sections in here you don't understand,
otherwise you could foul your build.
And finally, the src folder is where your React source is going to go.
Configuration.json File
I wanted to call this one out because honestly I couldn't find details on it anywhere else...
Javascript portlets like the React one I'm building can now access configuration properties from the Liferay system scope (i.e. global System Settings control panel) or portlet instance scope (the configuration panel from a typical Liferay portlet). Pretty cool, huh?
By defining your configuration in the configuration.json file, the Liferay UI will present your configuration dialog in the two places (System Settings or the portlet configuration dialog) for gathering config details, plus it will pass those details off to the portlet so you can leverage the configuration values in the portlet.
The file is defined by a JS schema, specifically this one: https://raw.githubusercontent.com/liferay/liferay-js-toolkit/master/resources/schemas/configuration.schema.json. It is kind of hard to read if you haven't seen one before, so I'll break it down here... There is a useful reference page here: https://github.com/liferay/liferay-frontend-projects/blob/master/maintenance/projects/js-toolkit/docs/configuration.json-file-reference.md
There are two primary objects, the system
object
and the portletInstance
object. Each contain the config
items for the target scopes, either the System Settings or the
portlet configuration dialog.
For the system object, there are two attributes,
category
and name
which will be the category and name you'll see in the System
Settings page. The portletInstance
object does not need
these attributes.
The fields object is part of both sections and contains the configuration key/definition pairs. The key is the configuration field name, it is what you'll reference in your JS code to get the value of the config item.
The definition is an object that contains a lot of items:
- type is the configuration type and can be one of number, float, string or boolean for both system and portletInstance items, but system can have an additional password type that portletInstance cannot. Before you ask, the answer is No, there is no way to add additional types to the configuration.
- Name and description are either the human readable values or the language bundle keys for the labels that will appear in the UI.
- default is the default value for the config item in case it has not been set. Be sure to use a value consistent with the type.
- options is an object of key/label pairs used when displaying a pick list. The key will be what the value of the configuration item is, and the label is what the user will see (either human readable or language bundle key work here). You only want to use the options when you are limiting to a list of items for the user to pick from; if you leave it off, they will see a basic text entry field.
Your React components will have access to the configuration
through the props.configuration
object. So the sample
data gives you two configuration fields, one "fruit" and
one "drink". You could initialize your state using these values:
state = { favoriteFruit: props.configuration.system.fruit, favoriteBeverage: props.configuration.portletInstance.drink };
You can also reference them in the props argument on your constructor if you have one.
Using Clay Components
We all love a good component library, right? For Liferay, one component library you might want to pick is ClayUI. Why? Well consistency is a good reason, right? Liferay as a whole is leveraging Clay all over the place, so if you use the Clay components, your portlet should be visually consistent with the rest of Liferay.
So how do you add the Clay components to your project? It's pretty easy actually... First, you want to find the list of components here: https://clayui.com/docs/components/index.html. When you find the component you want, have npm install it like I've done here for the ClayUI Table component:
$ npm install @clayui/table
Now if you see a warning (like you would for table) about not finding the peer ClayUI CSS component, you can just ignore that. And don't try to install the ClayUI CSS component either. The ClayUI CSS is already going to be loaded in as part of the theme.
You can selectively add ClayUI components to your module, or you can just add them all using the following:
$ npm install @clayui/alert @clayui/autocomplete @clayui/badge @clayui/breadcrumb \ @clayui/button @clayui/card @clayui/charts @clayui/color-picker \ @clayui/data-provider @clayui/date-picker @clayui/drop-down @clayui/empty-state \ @clayui/form @clayui/icon @clayui/label @clayui/link @clayui/list \ @clayui/loading-indicator @clayui/management-toolbar @clayui/modal \ @clayui/multi-select @clayui/multi-step-nav @clayui/nav @clayui/navigation-bar \ @clayui/pagination @clayui/pagination-bar @clayui/panel @clayui/popover \ @clayui/progress-bar @clayui/shared @clayui/slider @clayui/sticker @clayui/table \ @clayui/tabs @clayui/time-picker @clayui/tooltip @clayui/upper-toolbar
With these pulled in, you can then start importing components into your React files:
import ClayTable from "@clayui/table"; import Checkbox from "@clayui/form";
And also using them in your render() methods:
render() { return ( <div className="my-table"> <ClayTable> <ClayTable.Head> <ClayTable.Row> ...
The ClayUI site should have all of the details you need to leverage the components effectively in your React portlets.
Liferay JavaScript Object
Liferay has a suite of Javascript objects and services at your disposal, even if you are using React.
I spoke of some remote SB services I had created and used as part of one of my projects, for example. The way to invoke these kinds of services (and you'll find examples like this scattered throughout the Liferay OOTB portlets), is via code like:
_getEntries(callback) { var instance = this; Liferay.Service( '/assettag/get-groups-tags', { groupIds: instance.get('groupIds'), }, callback ); }
You can still do this in your React code, too!
Liferay has lots of other JS available such as
fire()
and on()
for InterPortlet
Communication (IPC), that works even with React code. You can access
Session object to extend the session, you can access the
themeDisplay object, ... Anything that's there, you can call it just
by using the Liferay
container.
Routing
If you have a really simple React portlet, routing may not be necessary.
Most of the time, though, you probably want to have routing so you can navigate within your portlet. When you do, you can't just use any Router implementation. When running under Liferay, Liferay owns the address bar, not your React portlet.
Avoid BrowserRouter at all costs. Instead, opt for the HashRouter (puts your routing stuff after a hash # on the url, Liferay will ignore that) or even better the MemoryRouter.
I always recommend the MemoryRouter because it is the best option since (a) Liferay owns the address bar and (b) two React portlets on the same page will trip over each other if they both use the HashRouter. The Liferay documentation, however, leans towards the HashRouter as it is the only address bar modification that Liferay supports. The caveat here is that you cannot have multiple JS-based portlets on the page each trying to use hash-based routing, they will clobber each other and break the routing for all.
Using MemoryRouter is pretty simple, just start with the import:
import React from 'react'; import { NavLink, Redirect, Route, MemoryRouter as Router, Switch } from 'react-router-dom';
With that in, you can build out your app with your navigation routes defined:
import React from 'react'; import { NavLink, Redirect, Route, MemoryRouter as Router, Switch } from 'react-router-dom'; import EditValues from './pages/page1/Edit'; import Home from './pages/index'; import Page1 from './pages/page1/index'; import Page2 from './pages/page2/index'; import Page2Create from './pages/page2/Create'; class App extends React.Component { render() { return ( <Router> <div> <nav className="navbar navbar-expand-lg navbar-light bg-light"> <ul className="navbar-nav"> <li className="nav-item"> <NavLink activeClassName="active" className="nav-link" exact to="/">Home</NavLink> </li> <li className="nav-item"> <NavLink activeClassName="active" className="nav-link" to="/page1">Page1</NavLink> </li> <li className="nav-item"> <NavLink activeClassName="active" className="nav-link" to="/page2">Page2</NavLink> </li> </ul> </nav> <Route component={Home} exact path="/" /> <Route component={Page1} exact path="/page1" /> <Route component={Page2} exact path="/page2" /> <Route component={Page2Create} exact path="/page2/create" /> <Switch> <Route component={EditValues} exact path="/page2/create" /> <Route component={EditValues} exact path="/page2/:id/edit" /> </Switch> </div> </Router> ); } } export default App;
Just follow the regular React rules for routing and you're good to go.
Unmounting the React Portlet
This is an addition from information provided by my friend Julien Mourad: If you do not have SPA disabled (you'll know if you did disable SPA), it is important that you unmount your React portlet.
With SPA enabled, Liferay does not do a full page refresh, it's doing AJAX calls back to Liferay and performing partial page updates, even when you navigate from page to page.
With SPA enabled, your React portlet will never get unmounted automatically as the user navigates around on pages, you have to handle the unmounting manually.
Fortunately that's pretty easy to do. In the index.js of your portlet, you're going to add 3 lines of code to the main() function. Here's how it looks for the sample code generated when the module was created:
export default function main({portletNamespace, contextPath, portletElementId, configuration}) { ReactDOM.render( <AppComponent portletNamespace={portletNamespace} contextPath={contextPath} portletElementId={portletElementId} configuration={configuration} />, document.getElementById(portletElementId) ); // Liferay will issue the destroyPortlet event when the // portlet should unmount. Liferay.once('destroyPortlet', () => { ReactDOM.unmountComponentAtNode(container); }); }
The last three lines are the ones that were added to trigger the unmount() call.
Although these lines are not needed when SPA is disabled (the full page refresh will always ensure the browser memory has been reset on every page render), I would actually recommend just adding them to all of your React portlets.
After all, you don't really know if an admin will come along and either enable or disable SPA without telling the developers. Adding these three lines will ensure that you're unmounted whether SPA is enabled or not.
Building the React Portlet
Building your React-based portlet couldn't be easier.
If you're using the standalone yo liferay-js
option, well you have an NPM project ready to create the portlet jar
and you just use the npm run build
command. If you're
using the Liferay Gradle Workspace, just do your regular
gradlew build
and it does the rest.
In both cases, you'll have two new folders in the root of your
project. First is the build
dir where all of the raw
artifacts are in the appropriate locations.
The second is the dist
folder. In here, you'll find
your React portlet jar file, ready to be plucked and sent off to a
remote Liferay server for deployment.
For your own development, you can use npm run
deploy
or gradlew deploy
and your React portlet
will be deployed to the local Tomcat bundle you pointed at when
creating the module using the generator.
Conclusion
So this is it, really. I know, I know, there's not a lot of React in here, but there doesn't need to be. The info here gives you the framework to build the next great React portlet, but that's gotta come from you. Using the liferay-js generator you have a solid foundation, you just need to add inspiration and some React code.
Thats all I have. If you have any React portlet tips you'd like to share, please add a comment below. If you have a question or comment, add those below too. If you find a mistake in what I wrote, keep it to yourself... Just kidding, I'd love your feedback below and will fix the content. Or of course you can find me in the community Slack and chat with me directly.