I would use global AngularJS with Liferay Portal. Because, like the devise of AngularJS:
Why AngularJS? HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is extraordinarily expressive, readable, and quick to develop.
I would simple use the declarative syntax of html by developing of Liferay-Theme and Portlets.
For this requirement I have created new Liferay-Theme and customized a little bit the portal_normal.vm
:
<!DOCTYPE html>
<#include init />
<html ... ng-app="liferay">
<head>
...
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js"></script>
<script type="text/javascript" src="${javascript_folder}/my.js" charset="utf-8"></script>
</head>
<body class="${css_class}">
<div ng-controller="LiferayCtrl">
here my.js
:
angular.module('liferay', [])
.controller('LiferayCtrl', function($scope) {
console.log("---==== Init AngularJS ====---");
$scope.Liferay = Liferay;
});
and I can extend the controller, like getting of Liferay-Site name.
What's all this in aid of?
Hereby I can simple access Liferay JavaScript values & functions over declarative html syntax, without direct JavaScript function calling, like AngularJS way.
E.g. now it is possible to get values and functions of Liferay JavaScript by declarative html code, like here, for getting current url in web content display:
Liferay current URL: {{Liferay.currentURL}}
However, my questions are:
First of all: nice idea using AngularJS on the portal itself. Until now we are using AngularJS only for portlets – I’ll explain later why and how.
There are not more side-effects using AngularJS as with other JavaScript libraries or frameworks. Liferay is shipped with AlloyUi (http://alloyui.com/). This JavaScript library is based on YUI (http://yuilibrary.com/) written by yahoo. YUI uses a different namespace than jQuery, so you can use jQuery along with AlloyUI. And if you are able to use jQuery without any side-effects you may use AngularJS even more, because AngularJS provides a subset of jQuery.
All you have to make sure is that you include jQuery before AngularJS if you need the full jQuery not only the jqLite implementation of AngularJS. This can easily be done by changing the portal_normal.vm
in your theme:
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>$the_title - $company_name</title>
<script type="text/javascript" src="$javascript_folder/jquery-2.0.3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="$javascript_folder/angular-1.2.7.min.js" charset="utf-8"></script>
$theme.include($top_head_include)
</head>
If you include the JavaScript files on the theme level you are able to use different versions for each theme. This is very handy if you have more than one portal instance and you don’t have the chance to use the same version in each instance.
It depends. One has to know, that Angular will parse the whole web page if you put your ng-app
directive on the html element, as in your example. If your page is very big and has a lot of content it could be a performance problem. So it is better put the ngApp directive somewhat down in your page and initialize the ngApp manually by:
$().ready(function() {
angular.bootstrap("css-selector", ['yourApp']);
});
There are two ways to achieve this.
1) On big ngApp as in your example. You have to know, that you can’t nest ngApps. So every portlet must be part of your app. With this approach you will lost the possibility that a portlet can provide an individual part to your page. All portlets need to know what the name of your ngApp is and must extend this ngApp. You may not use these portlets without changes in other portal server if they use another ngApp name or no one. The advantage is you can share the $rootScope
in all of your portlets and the portlets may communicate with each other in angular ways (e.g. $emit
, $on
, shared servcies, ...).
2) Every portlet comes with its own ngApp. In this case there are no dependencies. Every portlet may instantiate its own ngApp by angular.bootstrap
. Furthermore every ngApp is created only for a small part of the web page and only if it’s really needed.
In both ways you should avoid using routing. Because only one routeProvider
can be used per page.
We have decided to use the second approach so that the portlets keep individual and are not depending on a global ngApp.
You know you can configure your portlets. So the question comes up: “how do i provide my portlet preferences to my angular portlet?” For sure you could make an http request to get them. But this is not sufficient, because you make an unneeded call. Your portlets are usually java server pages so it would be nice to include the preferences during generation time on the server and provide these information to your angular app. But how?
In the doView
method of your portlet you may create a JSON-object and store it under a key in your RenderRequest
:
public void doView(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
PortletPreferences prefs = renderRequest.getPreferences();
Map<String, Object> values = new HashMap<String, Object>();
values.put(key, value);
renderRequest.setAttribute("config", new ObjectMapper().writeValueAsString(values));
super.doView(renderRequest, renderResponse);
}
In your jsp you are able to access this config attribute:
<div class="hidden" portlet-config>${config}</div>
portlet-config
is a directive that parses the JSON and stores the information in a service instance:
.factory('portletConfigService', function(){ return {}})
.directive('portletConfig', ['portletConfigService', function(portletConfigService) {
return {
restrict: 'A',
compile: function(elem, attrs) {
var config = angular.fromJson(elem.text());
angular.forEach(config, function(value, key){
portletConfigService[key] = value;
});
}
};
}])
Now you can inject the portletConfigService
to every controller, service, factory or whatever and make use of the config parameter: portletConfigService.key
.
Another problem we came around were the language properties. Normally you would define them in a language.properties
file and make use of them in your jsp. For example through fmt:message
tags. But you also want to be able to access them in your angular js app. And what you absolutely not want is to have them twice in your sources. We have solved this problem by a generated service and a directive that reads the properties for the current users language and creates the angular stuff in a jsp :
<%@page contentType="text/javascript; charset=UTF-8" %>
<%@ page import="java.util.ResourceBundle" %>
<%@ page import="java.util.Locale" %>
<%@ page import="java.util.Enumeration" %>
<%@ page import="org.apache.commons.lang.StringEscapeUtils" %>
angular.module('translations', [])
.factory('translations', function(){ return {
<%
ResourceBundle labels = ResourceBundle.getBundle("language", request.getLocale());
Enumeration<String> keys = labels.getKeys();
while(keys.hasMoreElements()){
String key = keys.nextElement();
out.write("\""+key+"\":\""+StringEscapeUtils.escapeJavaScript(labels.getString(key))+"\"");
if(keys.hasMoreElements()){
out.write(",\n");
}
}
%>
}})
.filter('mpbtranslate', ['translations', function(translations) {
return function(input) {
var translation = translations[input];
return translation? translation: '???'+input+'???';
};
}]);
You can now use these translations as a service and as a filter. For example <div>{{'title' | translate}}</div>
will for example be evaluated to ‘Titel’ on the client side.
I hope you got some useful information regarding your concerns.