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.

Leave a Reply

Your email address will not be published. Required fields are marked *


*