Building JS Portlets Part 2

Introduction

In part 1 of the blog series, we started a Vue.js portlet based in the Liferay Workspace, but actually there was no JS framework introduced just yet.

We're actually going to continue that trend here in part 2, but in this part we're going to tackle some of the important parts that we'll need in our JS portlets to fit them into Liferay.

Passing Portlet Instance Configuration

In part 1 our view.jsp page used the portlet instance configuration, displayed as two text lines:

<%@ include file="/init.jsp" %>

<p>
  <b><liferay-ui:message key="vue-portlet-1.caption"/></b>
</p>
<p><liferay-ui:message key="caption.flag.one"/> <%= 
  String.valueOf(portletInstanceConfig.flagOne()) %></p>
<p><liferay-ui:message key="caption.flag.two"/> <%= 
  String.valueOf(portletInstanceConfig.flagTwo()) %></p>

We're actually going to continue this, but we're going to leverage our <aui:script /> tag to leverage it in a new addition to the JSP:

<aui:script>

var <portlet:namespace/>portletPreferences = {
	flag: {
		one: <%= portletInstanceConfig.flagOne() %>,
		two: <%= portletInstanceConfig.flagTwo() %>
	}
};

</aui:script>

Using <aui:script />, we're embedding javascript into the JSP.

Inside of the script tag, we declare a new variable with the name portletPreferences (although this name is namespaced to prevent a name collision with another portlet on the page).

We initialize the variable as an object which contains our portlet preferences. In this example the individual flags have been set underneath a contained flag object, but the structure can be whatever you want.

The goal here is to create a unique Javascript object that will hold the portlet instance configuration values. We need to extract them in the JSP because once the code is over and running in the browser, it will not have access to the portlet config. Using this technique, we get all of the prefs in a Javascript variable so they will be available to the running JS portlet in the browser.

Passing Permissions

We're actually going to continue this technique to pass permissions from the back end to the JS portlet:

var <portlet:namespace/>permissions = {
	<c:if test="Validator.isNotNull(permissionChecker)">
		isSignedIn: <%= permissionChecker.isSignedIn() %>,
		isOmniAdmin: <%= permissionChecker.isOmniadmin() %>,
		hasViewOrgPermission: <%= permissionChecker.hasPermission(null, 
		  Organization.class.getName(), Organization.class.getName(), 
		  ActionKeys.VIEW) %>
	</c:if>
	<c:if test="Validator.isNull(permissionChecker)">
		isSignedIn: false,
		isOmniAdmin: false,
		hasViewOrgPermission: false
	</c:if>
};

Here we're trying to use the permission checker added by the init.jsp's <liferay-theme:defineObjects /> tag.

Although I'm confident I should never get a null permission checker instance, I'm using defensive programming to ensure that I can populate my permissions object even if the checker is not available.

When it is, I'm basically populating the object with keys for the permissions and then a scriptlet to evaluate whether the current user has the permission details.

Since we are building this as a Javascript object, we will be able to collect all of the permissions when the JSP is rendering the initial HTML fragment and allows us to ship the permissions back to the browser so the portlet can use the permissions to make decisions on what to view and what to allow editing on.

The only thing you need to figure out is what permissions you will need on the front end; once you know that, you can use this technique to gather those permissions and ship them to the browser.

NOTE: This does not replace the full use of the permission checker on the back end when processing requests. This technique passes the permissions to the browser, but as we all know it is easy for hackers to adjust these settings once retrieved in order to try to circumvent the permissions. These should be used to manage the UI, but in no way, shape or form should the browser control permissions when invoking services on the backend.

I18N

The previous sections have been fairly straight-forward; we have data on the backend (prefs and permissions) we need to have on front end but really only one way to pass them.

Handling the I18N in the JS portlets comes with a couple of alternatives.

It would actually be quite easy to continue the technique we used above:

var <portlet:namespace />languageKeys = {
	accessDenied: '<liferay-ui:message key="access-denied" />',
	active: '<liferay-ui:message key="active" />',
	localCustomMessage: '<liferay-ui:message key="local-custom-message" />'
};

I can tell you that this technique is extremely tedious. I mean, most apps have tens if not hundreds of language keys for buttons, messages, labels, etc. Itemizing them here will get the job done, but it is a lot of work.

Fortunately we have an alternative. Liferay actually provides the Liferay.Language.get(key) Javascript function. This function will conveniently call the back end to translate the given key parameter using the backend language bundle resolution. It is backed by a cache on the browser side so multiple calls for the same key will only call the backend once.

So, rather than passing the 'access-denied' message like we did above, we could just remember to replace hard-coded strings from our JS portlet's template code with calls like Liferay.Language.get('access-denied'). We would likely see the following for Vue templates:

var app5 = new Vue({
  el: '#app-5',
  data: {
    message: Liferay.Language.get('access-denied')
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

Although this is convenient, it too has a problem. Because of how the backend resolves keys, only resource bundles registered with the portal can be resolved. If you are only going after Liferay known keys, that's no problem. But to use your local resource bundle, you will need to register it into the portal per instructions available here: https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/overriding-language-keys#modifying-liferays-language-keys. Note that you're not overriding so much as adding your local resource bundle into the mix to expose your language bundle.

So actually to keep things simple I would recommend a mixed implementation. For existing Liferay keys, use the Liferay.Language.get() method to pull the value from the backend. But instead of registering an additional resource bundle in the portal, just use the script-based technique above to pass your local keys.

This will minimize your coding impact, but if your local bundle has tens or hundreds of your own keys, well you might find it easier to just register your language bundle and stick with Liferay.Language.get().

Conclusion

What what? We're at the end of part 2 and still no real Javascript framework integration?

That is correct, we haven't pulled in Vue.js yet, although we will be doing that in Part 3.

I think that it is important to note that from our original 6 prerequisites, we have already either fully or partially satisfied:

  • Prereq #1 - Using the Liferay Workspace.
  • Prereq #6 - Using the Liferay Environment.

The big conclusion here is that we can use this pattern to preload data from the portal side to include in the JS applications, data which is otherwise not available in the browser once the JS app is running.  You can apply this pattern for collecting and passing data that you want available to the JS app w/o providing a runtime fetch mechanism or exposing portal/portlet data.

Note: Of course in my JSP/Javascript stuff above, there is no verification of JS, no proper handling of string encoding and other protections you might use to ensure you are not serving up some sort of JS attack. Just because I didn't present it, doesn't mean that you shouldn't include it in your production apps.

See you in Part 3...