More Angular Adventures in Liferay Land

Last year I spent a lot of time looking into how AngularJS could be used in a portal environment, more specifically Liferay. That research culminated in a pretty technical and quite lengthy blog post: Angular Adventures in Liferay Land. The problem with a blog post that you invest such an amount of time in is that it always remains in the back of your head, especially if there were some things that you couldn't quite get to work like you wanted to. So like a lot of developers I keep tinkering with the portlet, trying to make it better, trying to solve some problems that weren't solved to my liking. 

So after a couple of months I ended up with a better portlet and enough material for a new blog post. In this second Angular/Liferay blog post I will address a couple of things:
  • the one thing I didn't quite get to work (and only found an ugly workaround for): routing
  • a better way to do i18n (after someone pointed out a problem with the current implementation)
  • validation (and internationalized validation) 
  • splitting up Javascript files, for readability, and merging them during the build
You can still find the example portlet on Github: angular-portlet
 

From here to there: the right way

During the testing I did for the previous post I wasn't able to get the widely used Angular UI Router module to work. I looked at a number of alternative modules, but no matter what I tried, I couldn't get any of them to work. So in the end I had to resort to a very ugly workaround that (mis)used ng-include to provide a rudimentary page switching capability. While it works and kinda does the job for a small portlet, it didn't feel right. During a consulting job at a customer that wanted to look into using AngularJS in a portal environment, the routing problem was one of the things we looked into together and managed to get working this time. 

To start off you need to download the UI Router JS file, add it to the project and load it using the liferay-portlet.xml:

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_2_0.dtd">

<liferay-portlet-app>
   <portlet>
      ...
      <footer-portal-javascript>/angular-portlet/js/angular-ui-router.js</footer-portal-javascript>
      ...
   </portlet>
</liferay-portlet-app>

After this it takes a couple of small, but important, changes to get similar UI Router code that I tried during the first tests, to work this time. Like I expected previously the UI Router $locationProvider needs to be put into HTML5 mode so it doesn't try to mess around with our precious portal URLs. This mode makes sure it doesn't try to change the URL, but it will still add a # to the URL sometimes. To counter this you also need to slightly tweak the $urlRouterProvider so it uses '/' as the otherwise option, something you also need to set on the url property of your base state (but not on the others): 

var app = angular.module(id, ["ui.router"]);

app.config(['$urlRouterProvider', '$stateProvider', '$locationProvider',
   function($urlRouterProvider, $stateProvider, $locationProvider) {

      ...
      $locationProvider.html5Mode(true);
      $urlRouterProvider.otherwise('/');

      $stateProvider
         .state("list", {
            url: '/',
            templateUrl: 'list',
            controller: 'ListCtrl'
         })
         ...
   }
]);
After these changes you'll be then able to use a pretty default $stateProvider definition, where we use the templateUrl field to define the page that is linked to the state. These changes alone still won't make the routing work. We still need another small change to Liferay to make the HTML5 mode work as this needs a correctly set base href in the HTML. This can be achieved in multiple ways in Liferay: a JSP override hook, a theme, ... . To keep everything nicely encapsulated in this portlet, I've chosen to use a simple JSP override in the portlet itself and not in a separate hook.
 
An override of /html/common/themes/top_head.jsp will enable you to add the required <base href='/'> tag on a global level to the portal HTML code:
<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util" %>

<%@ page import="com.liferay.portal.kernel.util.StringUtil" %>

<liferay-util:buffer var="html">
   <liferay-util:include page="/html/common/themes/top_head.portal.jsp" />
</liferay-util:buffer>

<%
   html = StringUtil.add(
         html,
         "<base href='/'>",
         "\n");
%>

<%= html %>
Just add this file to your portlet, with the correct subdirectories, in /src/main/webapp/custom_jsps and configure this directory in your liferay-hook.xml:
<?xml version="1.0"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 6.2.0//EN" "http://www.liferay.com/dtd/liferay-hook_6_2_0.dtd">
<hook>
    <custom-jsp-dir>/custom_jsps</custom-jsp-dir>
</hook>
With all this set up we now just need one more small change to tie it all together. The state configuration we currently have still doesn't take into account the fact that we're on a portal and need to use special URLs. Luckily the UI Router module has got us covered and provides a nice event, $stateChangeStart, that we can use to mess around with the URLs. We'll use this event to detect if the normal URL, used in the state, has already been adapted for portal use or not. If not we'll create a correct portal render URL based on the given template URL.
app.run(['$rootScope', 'url',
   function($rootScope, url) {

      $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
         if (!toState.hasOwnProperty('fixedUrl')) {
            toState.templateUrl = url.createRenderUrl(toState.templateUrl);
            toState.fixedUrl = true;
         }
      });
   }
]);
 
 
Now all the backend routing machinery is correctly set up, you can use the normal UI Router stuff on the frontend. You'll need to add the ui-view attribute to your base div in view.jsp:
...
<div id="<portlet:namespace />main" ng-cloak>
   <div ng-hide="liferay.loggedIn">You need to be logged in to use this portlet</div>
   <div ui-view ng-show="liferay.loggedIn"></div>
</div>
...
and then use the ui-sref attribute on your anchor tags to navigate:
<h2 translate>detail.for.bookmark</h2>
<form role="form">
   ...
   <button type="submit" class="btn btn-default" ng-click="save();" translate>action.submit</button>
   <button type="submit" class="btn btn-default" ui-sref='list' translate>action.cancel</button>
</form>
 

Found in translation

In the previous post this section was called Lost in translation and as yuchi pointed out in a Github issue he created for the example portlet Liferay.Language.get is synchronous and should be avoided when possible. So it seemed I was still a little bit lost in translation and needed to find a better solution. I think I might have found one in the Angular Translate module made by Pascal Precht. This looked like a great and more importantly configurable module that I might be able to get to work in a portal context.

To start off you need to download the Angular Translate JS file (and also the loader-url file), add it to the project and load it using the liferay-portlet.xml:
<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_2_0.dtd">

<liferay-portlet-app>
   <portlet>
      ...
      <footer-portal-javascript>/angular-portlet/js/angular-translate.js</footer-portal-javascript>
      <footer-portal-javascript>/angular-portlet/js/angular-translate-loader-url.js</footer-portal-javascript>
      ...
   </portlet>
</liferay-portlet-app>
By default it tries to find JSON files, that contain the translation key/value pairs, with certain URLs. So you can probably guess that by default this won't work. Luckily the module has all kinds of extensibility features built in and with a custom UrlLoader I was able to convince it to use my own custom language bundles:
app.config(['$translateProvider', 'urlProvider',
   function($translateProvider, urlProvider) {

      ...
      urlProvider.setPid(portletId);

      $translateProvider.useUrlLoader(urlProvider.$get().createResourceUrl('language', 'locale', Liferay.ThemeDisplay.getBCP47LanguageId()));
      $translateProvider.preferredLanguage(Liferay.ThemeDisplay.getBCP47LanguageId());
      ...
   }
]);
As you can see we just configure the $translateProvider to load the translations from a custom resource URL and use the current Liferay language ID as the preferred language. I also had to transform my Liferay URL factory, from the first version of the portlet, to a provider because you can't use factories/services in an Angular config section.
 
Using the resource URL mechanism from the first version of this portlet we can, with some classloader magic, output a JSON that represent a complete Liferay resource bundle (including extensions from hooks) for a given language:
@Resource(id = "language")
@CacheResource(keyParam = "locale")
public Map<String, String> getLanguage(@Param String locale) throws Exception {
   Locale localeValue = DEFAULT_LIFERAY_LOCALE;
   if (!Strings.isNullOrEmpty(locale)) {
      localeValue = Locale.forLanguageTag(locale);
   }

   ClassLoader portalClassLoader = PortalClassLoaderUtil.getClassLoader();

   Class c = portalClassLoader.loadClass("com.liferay.portal.language.LanguageResources");
   Field f = c.getDeclaredField("_languageMaps");
   f.setAccessible(true);

   Map<Locale, Map<String, String>> bundles = (Map<Locale, Map<String, String>>) f.get(null);

   return bundles.get(localeValue);
}
This code uses custom @Resource and @CacheResource annotations, which aren't specifically needed as a normal serveResource method will also do the trick, but these annotations make it just a little easier (and allow easy caching). More information about these annotations can be found in the Github code itself (in the /be/aca/liferay/angular/portlet/resource package).
 
With this new setup we can completely drop the custom translation directive we used in the previous version of the portlet. You now just need to add an empty translate attribute on any tag you want to translate (other ways are also possible and can be found in the Angular Translate docs). Adding this attribute to a tag will use the value between the tags as the key to translate:
<h2 translate>bookmarks</h2>
<div>
   ...
      <thead>
         <tr>
            <th translate>table.id</th>
            <th translate>table.name</th>
            <th translate>table.actions</th>
         </tr>
      </thead>
...
 

The importance of being valid (in any language)

After getting routing and translations to work I discovered another piece of functionality that was missing from the example portlet: validation. After looking around for Angular validation modules I settled on the Angular auto validate module by Jon Samwell. This seemed to be a pretty non intrusive and easy to use way to add validation to an Angular app. You just need to add a novalidate and ng-submit attribute to your form, some required, ng-maxlength, etc... attributes to your input fields and you're already done on the HTML side:

<form role="form" name="bookmarkForm" ng-submit="store();" novalidate="novalidate">
   <div class="form-group">
      <label for="name" translate>label.name</label>
      <input type="text" ng-model="model.currentBookmark.name" class="form-control" id="name" name="name" ng-minlength="3" ng-maxlength="25" required>
   </div>
   <div class="form-group">
      <label for="description" translate>label.description</label>
      <input type="text" ng-model="model.currentBookmark.description" class="form-control" id="description" name="description" ng-minlength="3" required>
   </div>
   <div class="form-group">
      <label for="url" translate>label.url</label>
      <input type="url" ng-model="model.currentBookmark.url" class="form-control" id="url" name="url" required>
   </div>
   ...
</form>
On the Javascript side of the code you need to add is also pretty minimal. Download the correct jcs-auto-validate.js file, add it to the project and load it using liferay-portlet.xml:
<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_2_0.dtd">

<liferay-portlet-app>
   <portlet>
      ...
      <footer-portal-javascript>/angular-portlet/js/jcs-auto-validate.js</footer-portal-javascript>
      ...
   </portlet>
</liferay-portlet-app>
Now that we have working validation it would be nice if the validation messages would be correctly translated using our custom language bundles instead of the ones bundled with the Javascript module itself. Using a custom ErrorMessageResolver, that uses the Angular Translate module's service we've already used in the previous section, this can be easily achieved:
angular.module("app.factories").

   // A custom error message resolver that provides custom error messages defined
   // in the Liferay/portlet language bundles. Uses a prefix key so they don't clash
   // with other Liferay keys and reuses the code from the library itself to
   // replace the {0} values.
   factory('i18nErrorMessageResolver', ['$q', '$translate',
      function($q, $translate) {

         var resolve = function(errorType, el) {
            var defer = $q.defer();

            var prefix = "validation.";
            $translate(prefix + errorType).then(function(message) {
               if (el && el.attr) {
                  try {
                     var parameters = [];
                     var parameter = el.attr('ng-' + errorType);
                     if (parameter === undefined) {
                        parameter = el.attr('data-ng-' + errorType) || el.attr(errorType);
                     }

                     parameters.push(parameter || '');

                     message = message.format(parameters);
                  } catch (e) {}
               }

               defer.resolve(message);
            });

            return defer.promise;
         };

         return {
            resolve: resolve
         };
      }
   ]
);
You just need to add this custom resolver to the validator like this:
var app = angular.module(id, ["jcs-autoValidate"]);

app.run(['validator', 'i18nErrorMessageResolver',
   function(validator, i18nErrorMessageResolver) {

      validator.setErrorMessageResolver(i18nErrorMessageResolver.resolve);
   }
]);
 

Breaking up

In the previous version of the portlet most of the Javascript code was in a limited number of files that, with all the additional changes from this post, would've gotten pretty long. To keep everything short and sweet, we need to split up the existing files into smaller ones in a way that makes them easy to manage. In the Javascript world there are tools like Grunt that can do this, but they're not easy to use in a Maven based project. After looking around I settled on the WRO4J Maven plugin. This enables you to easily merge multiple Javascript (or CSS, ...) files into one, but it can also do other stuff like minimize them etc... . 

The first thing we need to do to make this work is add some stuff to the build plugins section of our pom.xml:

...
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-war-plugin</artifactId>
   <version>2.6</version>
   <configuration>
      <!--
         Exclude originals of file that will be merged with wro4j
      -->
      <warSourceExcludes>js/controller/*.js,js/service/*.js,js/directive/*.js</warSourceExcludes>
   </configuration>
</plugin>
<plugin>
   <groupId>ro.isdc.wro4j</groupId>
   <artifactId>wro4j-maven-plugin</artifactId>
   <version>1.7.7</version>
   <executions>
      <execution>
         <phase>compile</phase>
         <goals>
            <goal>run</goal>
         </goals>
      </execution>
   </executions>
   <configuration>
      <!-- No space allowed after a comma -->
      <targetGroups>controllers,services,directives</targetGroups>
      <jsDestinationFolder>${project.build.directory}/${project.build.finalName}/js</jsDestinationFolder>
      <contextFolder>${basedir}/src/main/webapp/</contextFolder>
   </configuration>
</plugin>
...
The first plugin will make sure non of the original, split up, JS files, will be packaged in the WAR while the second will actually merge the different files, using a wro.groovy definition file in the WEB-INF directory, and make sure they are placed in the correct location in the target directory before the actual WAR is made.
groups {
    controllers {
        js(minimize: false, "/js/controller/Init.js")
        js(minimize: false, "/js/controller/*Controller.js")
    }
    services {
        js(minimize: false, "/js/service/Init.js")
        js(minimize: false, "/js/service/*Factory.js")
        js(minimize: false, "/js/service/ErrorMessageResolver.js")
    }
    directives {
        js(minimize: false, "/js/directive/Init.js")
        js(minimize: false, "/js/directive/*Directive.js")
    }
    all {
        controllers()
        services()
        directives()
    }
}
Now we just need to refer to the merged files this Maven plugin will create during packaging in the liferay-portlet.xml:
<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_2_0.dtd">

<liferay-portlet-app>
   <portlet>
      ...
      <footer-portlet-javascript>/js/controllers.js</footer-portlet-javascript>
      <footer-portlet-javascript>/js/services.js</footer-portlet-javascript>
      <footer-portlet-javascript>/js/directives.js</footer-portlet-javascript>
      ...
   </portlet>
</liferay-portlet-app>
For each of the groups in the Groovy definition file we need to provide a small Init.js file that is used to define a module with a name and the [] notation (so that an empty module is declared). Below is an example for the controllers that uses app.controllers as the module name:
'use strict';

var module = angular.module('app.controllers', []);
This way you can then easily define an actual controller using by just referring to the declared module name, without redeclaring it:
angular.module('app.controllers').
   controller("ListCtrl", ['$scope', '$rootScope', '$http', '$timeout', 'bookmarkFactory', '$stateParams',

      function($scope, $rootScope, $http, $timeout, bookmarkFactory, $stateParams) {
         ...
      }
   ]
);
You can then refer to this module in the bootstrap of your Angular app as follows:
var app = angular.module(id, ["app.controllers"]);
The same mechanism can be used for factories, services, directives, ... . 
 

A full Eclipse

One of the people that contacted me after the first blog post was Gregory Amerson. Greg works for Liferay and is the main developer of Liferay IDE (based on Eclipse). He tried out my portlet in Liferay IDE together with an Eclipse AngularJS plugin and encountered a couple of small problems: calling a function delete is problematic, some additional dependencies are needed for full taglib support, etc... . The most important changes discovered during his tests are merged back into this new version of the portlet. I also tried out Liferay IDE myself and I must say the HTML/JS/Angular support has been markedly improved, but in the end I still like my IntelliJ better.
 

Conclusion

With the routing module working now, better internationalisation, validation and JS file merging, AngularJS is starting to look more and more as a tool that a portal developer could add to his toolbelt and be productive with.

 

More blogs on Liferay and Java via http://blogs.aca-it.be.

 

 
 
 
 
Blogs
Hi Jan,

This is absolutely awesome. I gave up on using liferay and angular without trying as you have and gave up. This looks really good!

However, I tried getting your code from github and deploying it to liferay and unfortunately it isn't working emoticon

This is the error I get when just loading the initial page:

16:08:16,776 ERROR [http-bio-8080-exec-351][render_portlet_jsp:132] null
javax.portlet.PortletException: Path /partials/list.html is not accessible by this portlet
at com.liferay.portal.kernel.portlet.LiferayPortlet.checkPath(LiferayPortlet.java:194)
at com.liferay.util.bridges.mvc.MVCPortlet.include(MVCPortlet.java:360)
at com.liferay.util.bridges.mvc.MVCPortlet.include(MVCPortlet.java:378)
at com.liferay.util.bridges.mvc.MVCPortlet.doDispatch(MVCPortlet.java:311)

Any idea why this might be happening?
Ok Jan, never mind I fixed it...

By overriding the checkPath method implemented in the MVCPortlet as follows :


@Override
protected void checkPath(String path) throws PortletException {
//do nothing
}

But this doesn't seem optimal. Anyway all seems to be working. Great job.
Hi Robert,

Glad my post is of use.

I haven't encountered this error myself (yet). Maybe it's something that happens in a specific Liferay version. Which version are you trying it on?
I am facing the exact same error message. Could you please explain how you did the overriding, since this function is located in the com.liferay.portal.kernel package, which cannot be overridden out of the box with a liferay-hook, right? I am quite new to Liferay, so sorry if the answer to this question is rather obvious. By the way, I am using Liferay 6.2.4.
Hi Steve,

I tried my latest version, using in my Liferay Devcon presentation, in a Liferay 6.2.4 CE, but didn't encounter the issue. The code I tried was the rest-promises portlet from https://github.com/planetsizebrain/angular-adventures (you'll also need to deploy the angular-hook).

When looking at the code of the MVCPortlet class, it looks like you can just subclass it with a class in your own portlet and override the implementation of the checkPath method with your own.

When even looking a bit deeper into the original implementation of the checkPath method it seems that the PortalUtil.isValidResourceId method might be the reason for the error. It uses a portal property, portlet.resource.id.banned.paths.regexp
, to ban certain URLs. You might try changing the default value (to empty for example) for this property to see if that helps.
Hey Robert & Jan,
thanks for the fast reply and the detailed information of how to proceed in order to get ui-router functionality to work in portlet environment. As far as I could figure out, the error occurs because of a validity check forcing the html resource to be located under META-INF/resources, at least in version 6.2.4. Indeed, I'm working under ubuntu so I will see, if I experience any issues too.
Hey Steve... I am also using liferay 6.2.4 CE. The fix I did was to add :

@Override
protected void checkPath(String path) throws PortletException {
//do nothing
}

to the ResourcePortlet class which is in the github angular project . This is in the be.aca.liferay.angular.portlet.resource package. So you override in the ResourcePortlet class which is a sub-class of com.liferay.util.bridges.mvc.MVC$

You will only need to change the ResourcePortlet class and this should work.

...

By the way, are you by any chance running linux/ubuntu? Me and a friend have both had this issue on ubuntu setups. Doubt it has to do with it but worth a shot
Hey Jan,

Thank you for your awesome article.

I'm building a little Liferay-AngularJS project myself, and your samples provide a wonderful foundation to that.

When I tried to build the example with mvn, stumbled on this error:
Caused by: java.io.FileNotFoundException: c:\angular-portlet\src\main\webapp\WEB-INF\wro.xml (File could not be found)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:146)
at ro.isdc.wro.extensions.model.factory.SmartWroModelFactory.createAutoDetectedStream(SmartWroModelFactory.java:166)
... 46 more
[WARNING] Couldn't load new model, reusing last Valid Model!
[ERROR] Exception occured while processing: ro.isdc.wro.WroRuntimeException: No valid model was found!, class: ro.isdc.wro.WroRuntimeException,caused by:
ro.isdc.wro.WroRuntimeException: No valid model was found!

The interesting thing is that it looks for wro.xml, but this file is indeed not existing. Instead, a wro.groovy file is under the same path, as is it described in your post.

Do you guys have an idea how to overcome this?

I searched various forums related to Maven and Wro, but could not find any solution so far.

Thank you very much!
Hi István,

I just tried to build the latest download of the master branch of the example portlet and for me the build went fine and the WRO4J Maven plugin did its job. What I did notice was that even though it works and uses my wro.groovy file, it also mentions wro.xml in the output.

What you could try is to add <wroFile>${basedir}/src/main/webapp/WEB-INF/wro.groovy</wroFile> to the configuration section of the wro4j plugin. I did this myself locally and this also works for me.