Making a AngularJS based Liferay portlet instanceable

In one of my previous posts I showed how to create a Liferay portlet based on AngularJS. The problem with that concept was, that only a single portlet of that kind could be placed on a portal page.
I will show now how to improve the concept to not only allow multiple AngularJS portlets on a single Liferay page, but to allow the same portlet multiple times on a page.

The key is to manually bootstrap the AngularJS apps on the page, so we can no longer simple use ng-app. Instead of:

  <div ng-app>
     
  </div>

We assign an ID to our root DIV und attach the AngularJS module to it:

<div id="angularjsPortletDemo">
 
</div>

<script type="application/javascript" th:inline="javascript">
  (function() {
    if (typeof(AUI) !== 'undefined') {
      /* We are within Liferay */
      AUI().ready(function() {
        startAngular();
      });
    } else {
      document.addEventListener("DOMContentLoaded", function(event) {
        startAngular();
      });
    }

    function startAngular() {
      var appRootElem = document.getElementById('angularjsPortletDemo');
      angular.bootstrap(appRootElem, ['angularjsPortletDemo']);
    }
  })();
</script>

To make the portlet acutally instanceable we have to do more:

  1. Get rid of the global JavaScript variables in the HEAD (which we used to pass parameters from the backend such as the AJAX URL).
  2. Replace the ID on the root DIV by the actual Liferay portlet ID
  3. Change the Liferay portlet configuration

For the first step we just move the global variables into the IIFE block where the AngularJS bootstrapping happens. And we pass the variables to AngularJS as values to be able to use it in the AngularJS dependency injection system:

function startAngular() {
    var ajaxURL = /*[[${ajaxURL}]]*/ "/testdata/";
    var isStandalone = /*[[${standalone}]]*/ true;
    var authenticatedUser = /*[[${authenticatedUser}]]*/ "anonymous";

    var app = angular.module('angularjsPortletDemo');
    app.value('ajaxUrl', ajaxURL);
    app.value('isStandalone', isStandalone);
    app.value('authenticatedUser', authenticatedUser);

    var appRootElem = document.getElementById('angularjsPortletDemo');
    angular.bootstrap(appRootElem, ['angularjsPortletDemo']);
}

For the second step we use Thymeleaf to inject the portletId into the HTML template:

@RenderMapping
public String view(RenderRequest request, RenderResponse response, ModelMap model) {
  //...
  ThemeDisplay themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
  PortletDisplay portletDisplay = themeDisplay.getPortletDisplay();
  model.addAttribute("portletId", portletDisplay.getId());
}
<div id="angularjsPortletDemo" th:id="${portletId}" 

</div>

var portletId = /*[[${portletId}]]*/ 'angularjsPortletDemo';

var appRootElem = document.getElementById(portletId);
angular.bootstrap(appRootElem, ['angularjsPortletDemo']);

The last and most simple step to make the portlet instantiable is to set the homonymous attribute in liferay-portlet.xml:

<portlet>
    <portlet-name>liferay-angularjs-portlet</portlet-name>
    <instanceable>true</instanceable>
    <requires-namespaced-parameters>false</requires-namespaced-parameters>
    <ajaxable>true</ajaxable>
    <header-portlet-css>/css/app.css</header-portlet-css>
    <footer-portlet-javascript>/js/vendor.js</footer-portlet-javascript>
    <footer-portlet-javascript>/js/app.js</footer-portlet-javascript>
</portlet>

And the best of all: The AngularJS app can still be launched standalone: Just run grunt server on the console!

The full code can be found on GitHub.

Creating a Liferay portlet based on AngularJS

After my post about converting an existing AngularJS app into a portlet I’ve got a lot of requests about how to integrate the portlet with the portal backend.
So I’ve created a little demonstrator and put in on GitHub: https://github.com/nonblocking/liferay-angularjs-portlet. It uses Spring Portlet MVC and thymeleaf as template engine as backend.

Communication to the backend

The AJAX communication with the backend is based on resource requests which are originally intended to serve static resources such as images and JavaScript (see: http://www.oracle.com/technetwork/java/jsr286-2-141964.html#Serving_Resources).

To pass the resource URL (which is generated and quite complicated) to the AngularJS app the demonstrator uses thymeleaf’s Script Inlining support. Within the head section of index.html:

<script type="text/javascript" th:inline="javascript">
  var angularJsPortletAjaxURL = /*[[${ajaxURL}]]*/ "/testdata/";
  var angularJsPortletStandalone = /*[[${standalone}]]*/ true;
  var angularJsPortletAuthenticatedUser = /*[[${authenticatedUser}]]*/ "anonymous";
</script>

And within the controller method responsible for the View phase:

@RenderMapping
public String view(RenderRequest request, RenderResponse response, ModelMap model) {
  User user = (User) request.getAttribute(WebKeys.USER);
  String userScreenName = user != null ? user.getScreenName() : "anonymous";

  ResourceURL baseResourceUrl = response.createResourceURL();

  model.addAttribute("ajaxURL", baseResourceUrl.toString());
  model.addAttribute("standalone", false);
  model.addAttribute("authenticatedUser", userScreenName);

  return "index";
}

To invoke a specifc controller method and fetch some data a POST request is sent to the resource URL extended by the resource id (&p_p_resource_id={myResourceId}).
The code within AngularJS looks like this:

var method = 'users';
var params = { "startIndex": startIndex, "limit": limit };

$http({
  url: angularJsPortletAjaxURL + "&p_p_resource_id=" + method,
  method: 'POST',
  params: params
})
.success(function(data) { 
/* ... */ 
});

And the AJAX call will invoke the controller method with the corresponding ResourceMapping annotation:

@RenderMapping
@ResourceMapping("users")
public void users(@RequestParam int startIndex, 
  @RequestParam int limit, ResourceResponse response) throws Exception {
    UserList users = /* fetch the users */

    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");

    //Automatic JSON serialization doesn't work yet in Protlet MVC
    //See: https://jira.spring.io/browse/SPR-7344
    JSON_MAPPER.writeValue(
      response.getPortletOutputStream(), users);
}

Javascript and CSS resources

The JavaScript and CSS resources are not directly added to the HTML, but rather declared in liferay-portlet.xml like this:

<portlet>
  <!-- ... --> 
  <header-portlet-css>/css/app.css</header-portlet-css>
  <footer-portlet-javascript>/js/vendor.js</footer-portlet-javascript>
  <footer-portlet-javascript>/js/app.js</footer-portlet-javascript>
</portlet>

Standalone Mode

A very interesting opportunity when developing a HTML5 portlet is that you can run it standalone. This greatly accelerates the development. Since thymeleaf templates are pure HTML it’s no big deal at all.

In the demonstrator I’ve added the JavaScript and CSS resources, which are normally added by the portal, to the HTML (index.html) too. But only in the case the app runs in standalone mode. If the standalone property is set within the portlet controller the script tag is not rendered:

 <script type="application/javascript" src="js/app.js" 
   th:if="${standalone}">
</script>

Secondly I’ve created a test backend which delivers test data (JSON) when the portal is not available. There actual implementation is selected based on the global angularJsPortletStandalone variable which will also be rewritten by thymeleaf:

.factory('backend', function($http) {
  var portletBackend = /* ... */;

  var testBackend = /* ... */;

  if (angularJsPortletStandalone) {
    return testBackend;
  } else {
    return portletBackend;
  }
});

The test backend also comes in handy when you use Jasmine to unit test your JavaScript code (something you should definitely consider).

I hope the demo portlet is helpful. Don’t hesitate to post remarks and comments.

Convert an existing AngularJS App into a Liferay Portlet

As an experiment I tried to deploy an existing AngularJS Application on an Liferay Portal Server. Just to see if it would be possible to use HTML 5 + JSON Services for Portlet development too.
It turned out to be quite simple, but it feels a bit weird of course. For example AngularJS uses hashbangs and they make browser navigation suddenly available on the portlet level.
The approach is also limited, because it might be difficult to have multiple HTML 5 portlets on a single portal page. The DOM manipulation would most likely not work properly. But it could be interesting if you provide portlets for mobile devices that run maximized anyway.

Below a step by step guide:

1. Convert the front-end

Let’s say your index.html looks like this:

<!doctype html>
<html ng-app="my-angularjs-app">
  <head>
    <link rel="stylesheet" href="css/app.cs">
  </head>
  <body>
    <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

    <script type="text/javascript" src="js/app.js"></script>
  </body>
</html>

To run this as a portlet you have to do the following:

  1. Create a Java Web-Application as usual and copy all your resources into it
  2. In the document root create a view.jsp and copy everything within the body of index.html into it
  3. Wrap the content of view.jsp with an additional div layer and add the attribute ng-app=”my-angularjs-app”
  4. Create the usual portlet configuration files under WEB-INF
  5. Configure Liferay to add the Javascript to the footer of every portal page that contains the portlet
  6. Configure Liferay to add the Stylesheet to the header section of every portal page that contains the portlet

Here the view.jsp:

<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>

<portlet:defineObjects />

<div ng-app="my-angularjs-app">

  <!-- The body of index.html -->
  <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

</div>

The portlet.xml and liferay-display.xml config files are straightforward:

<?xml version="1.0"?>
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0">
    <portlet>
        <portlet-name>my-angularjs-app-portlet</portlet-name>        
        <display-name>my-angularjs-app-portlet</display-name>
        <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>        
        <init-param>
            <name>view-jsp</name>
            <value>/view.jsp</value>
        </init-param>
        <expiration-cache>0</expiration-cache>
        <supports>
            <mime-type>text/html</mime-type>
        </supports>
    </portlet>
</portlet-app>
<?xml version="1.0"?>
<!DOCTYPE display PUBLIC "-//Liferay//DTD Display 6.1.0//EN" "http://www.liferay.com/dtd/liferay-display_6_1_0.dtd">
<display>
    <category name="AngularJSPortlets">
        <portlet id="my-angularjs-app-portlet" />
    </category>
</display>

In liferay-portlet.xml we need to include the javascripts and stylesheets from the index.html:

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">
<liferay-portlet-app>
	<portlet>
		<portlet-name>my-angularjs-app-portlet</portlet-name>
		<header-portlet-css>/css/app.css</header-portlet-css>
		<footer-portlet-javascript>/js/app.js</footer-portlet-javascript>		
	</portlet>
</liferay-portlet-app>

If you have multiple template files in your application you might have to fix the path to them, because relative URLs won’t work in Portal environment. The URLs need to point to the context of your deployed application. For example, if you have a route like this:

$routeProvider.when('/page1',
  {templateUrl: 'page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: 'page2.html', controller: 'Page2Ctrl'});

They should now look like this:

$routeProvider.when('/page1', 
  {templateUrl: '/my-application/page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: '/my-application/page2.html', controller: 'Page2Ctrl'});

I know, that’s a bit odd, to hardcode an application context, but you could make the path configurable.

That’s basically it for the front-end. If you deploy your application within a WAR on Liferay you should be able to add your portlet to a portal page.

2. Integrate the back-end

Essentially, there are two possibilities here:

  1. Call remote REST services. Here you could get troubles with the Same-Origin-Policy, but fortunately, AngularJS supports JSONP. Is you choose this solution you’ve nothing else to do.
  2. Deploy the back-end (the services) together with your portlet. You cannot get problems with the Same-Origin-Policy and it is even possible to obtain the currently authenticated portal user in the service code.

I chose option 2, and deployed the Spring MVC REST back-end as part of the portlet. To hide the application context of the portlet and to be able to obtain the shared session attributes of Liferay (e.g. the authenticated user) I’ve used Liferay’s PortalDelegateServlet:

In web.xml of the portlet application:

<servlet>
  <servlet-name>portalDelegateServlet</servlet-name>
  <servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
    <init-param>
      <param-name>servlet-class</param-name>
      <param-value>org.springframework.web.servlet.DispatcherServlet</param-value>
    </init-param>
    <init-param>
      <param-name>sub-context</param-name>
      <param-value>rest-services</param-value>
    </init-param>
    <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:/META-INF/webapp-context.xml</param-value>
    </init-param>        
  <load-on-startup>1</load-on-startup>
</servlet>

If you’ve a service defined like this in Spring MVC:

@RequestMapping("/rest-services")
public class MyService {
  @RequestMapping("/customers")
  public List<Customer> customers() {
    //...
  }
}

You can now access your the service under:

http://<liferay-portal>/delegate/rest-services/customers

Please note: This only works, if the sub-context in the delegate definition matches the path prefix of the services.

The advantage of using the delegate (instead of directly accessing the service in the context of the webapp containing your portlet) is that could can obtain the currently authenticated Liferay user like this:

request.getSession().getAttribute("USER_ID")

3. Alternative approach with Spring Portlet MVC

I also tried to use Spring Portlet MVC to get a more traditional portlet setup. It would also have the advantage that the back-end is actually container authenticated and e.g. request.isInRole() could be used.

The problem was, it required a lot of additional configuration and a specific Portlet controller. Also, “normal” URLs couldn’t be used for service calls, they needed to be constructed as Action URLs like this:

<portlet:actionURL var="serviceURL" >
    <portlet:param name="serviceName" value="myServiceName"/>
</portlet:actionURL>
   // Javascript
   var url = "${serviceURL}";

At the end it was too much effort for me to get it running.