Blogs
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.

- 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
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'})...}]);
<%@ 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 %>
<?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>
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;}});}]);
...<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>...
<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.
<?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>
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());...}]);
@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);}
<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>
<?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>
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};}]);
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>...
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()}}
<?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>
'use strict';var module = angular.module('app.controllers', []);
angular.module('app.controllers').controller("ListCtrl", ['$scope', '$rootScope', '$http', '$timeout', 'bookmarkFactory', '$stateParams',function($scope, $rootScope, $http, $timeout, bookmarkFactory, $stateParams) {...}]);
var app = angular.module(id, ["app.controllers"]);
A full Eclipse
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.

