Angular Adventures in Liferay Land

Intro

A lot of developers will probably know this feeling: you've just returned from a conference and seen lots of exciting new technologies and now you can't wait to try them out! For me one such moment and technology was Devoxx 2013 and seeing an AngularJS talk by Igor Minar and Misko Hevery. The speed and ease of development appealed very much to me, but I was a bit hesitant because of the language choice: Javascript. Javascript and me have been a bit of reluctant friends that tolerate each other, but don't need to spend a lot of time together (a bit like Adam and Jamie of Mythbusters fame). I usually try to hide all Javascript complexity behind the frameworks I use, e.g. JSF/Primefaces.  Nonetheless I did see and love the inherit challenge of trying to use a new framework and see how it can be used in what I do on a daily basis: customizing and writing portlets to use in a Liferay portal. So for this blog post I forced myself to do the following:

As mentioned before, we usually write JSF based portlets to use in Liferay and the JSF implementation of our choice at ACA has been Primefaces for a couple of years now (as far as JSF frameworks go a good choice it seems as Primefaces seems to have 'won' the JSF framework battle). With JSF being an official specification for web applications and there also being a specification now to bridge JSF to work nicely in a portal enviroment this combination has served us well. With AngularJS there will be none of these things. AngularJS is a standalone JS framework that is on one side side pretty opinionated about certain things (which usually means trouble in a portal environment), but on the other side is pretty open to customization and changes.
 
This blog post will try to show how AngularJS can be used to build a simple example portlet, integrate it with Liferay (services, i18n, IPC, ...), show what the technical difficulties are and how these can be overcome (or worked around). The full code of this portlet is available on Github under the MIT license: angular-portlet. Feel free to use it as a base for further experiments, as the base for a POC, whatever... and comments/ideas/questions/pull requests are welcome too.
 
Now on to the technical stuff!
 

Lots & lots of Javascript

The first hurdle which I thought might give problems was using an additional Javascript framework in an environment that already has some. Liferay itself is built on YUI & AlloyUI and provides those frameworks globally to every portlet that runs in the portal environment. Liferay used to use JQuery and will be using JQuery again in future versions, but even in the current version, Liferay 6.2, it is perfectly possible to use JQuery in your portlets if you use it in noConflict mode. Also Primefaces, which uses JQuery, works fine in a Liferay portal. 
 
AngularJS will work with JQuery when it is already provided or fall back to an internal JQLite version when JQuery isn't provided. So when I added AngularJS to a simple MVCPortlet and used it in a standard Liferay 6.2 (which has YUI & AlloyUI, but no JQuery) my fears turned out to be unwarranted: everything worked just fine and there were no clashes between the Liferay and Angular javascript files.
 

Not alone

The next step I tried to take was to try and take the simple Hello World! application that is built in the AngularJS learn section of their website and here I immediately ran into an incompatibility: AngularJS, with its ng-app attribute, pretty much assumes it is alone on the page. In a portal, like Liferay, a portlet is usually not alone on the page. It might be, but it may never assume this. The ng-app attribute is what AngularJS uses to bootstrap itself. So long as our custom AngularJS portlet is alone on a page this will work (as is demonstrated by other people that tried this), but once you add a second AngularJS portlet to the page (or a second portlet instance of the same portlet), the automatic bootstrapping via the attribute will cause problems.
 
Looking around it was immediately clear that I'm not the first person that tried to use AngularJS to build portlets:
All these people ran into the same problem and solved it pretty similarly: don't let AngularJS bootstrap itself automatically via the attribute, but call the bootstrapping code yourself and provide it with the ID of the element in the page that contains the Angular JS app GUI. Because we're working in a portal environment and possibly using instanceable portlets this ID needs to be unique, but this problem is easily solved by using the <portlet:namespace/> tag that is provided by the portal in a JSP and is unique by design. 
 
In your main Javascript code add a bootstrap method that takes the unique ID of the HTML element (something prefixed with the portlet namespace) and the portlet ID (aka the portlet namespace). Inside this method you can do or call all the necessary AngularJS stuff and end with the bootstrap call.
function bootstrap(id, portletId) {

    var module = angular.module(id);

    // add controllers, etc... to module
    module.controller("MainCtrl", ['$scope', function($scope) {
            // do stuff
        }]
    );

    angular.bootstrap(document.getElementById(id),[id]);
}

From your JSP page you can simply call this as follows:

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

<portlet:defineObjects />

<div id="<portlet:namespace />main" ng-controller="MainCtrl" ng-cloak></div>

<aui:script>
    bootstrap('<portlet:namespace />main', '<portlet:namespace />');
</aui:script>

Bootstrapping AngularJS like this allowed me to create a simple, instanceable, Hello World! AngularJS portlet that you could add multiple times to the same page and that would work independent of each other.

From here to there

The next problem is the one I knew beforehand would cause the most problems: navigating between pages in an AngularJS app. The problem here is that AngularJS assumes control over the URL, but in a portal this is a big no-no. Everything you try to do with URLs needs to be done by asking the portal to create a portal URL for your specific portlet and action. If you don't do this and mess with the URL yourself you might at first think everything works as expected, especially with a single AngularJS portlet on the page, but with multiple you'll quickly see things will start to go wrong.

I first started off trying to make the default AngularJS routing work correctly in a portlet, at first by creating portlet specific URLs (using the namespace stuff mentioned before), and later by trying to use the HTML5 mode, but whatever I tried, I couldn't get it to work completely and consistently. After this I Googled around a lot and found several other AngularJS routing components that can be used as a replacement for the original, but here too I couldn't get them to work like I wanted.

I still think that one of these should work and I probably made one or more mistakes while trying them out, but due to time constraints I opted to go for a simple, albeit hacky, solution: using an internal page variable combined with the AngularJS ng-include attribute. The reason I settled on this is that portlets are meant to be relatively small pieces of functionality that can be used in combination with each other (possibly using inter portlet communication) to provide larger functionality. This means there will usually only be limited navigation needed in a portlet between only a small number of pages, which lets us get away with this hack without compromising the normal AngularJS workings and development speed too much.

To make this hack work you just need to add an inner div to the one we already had and add the ng-include and src attribute to it and point the src attribute to a value on your model, called page in the example, that contains whatever piece of html you want to show.
<div id="<portlet:namespace />main" ng-controller="MainCtrl" ng-cloak>
    <div ng-include src="page"></div>
</div>

In your Javascript code you only need to make sure that this page field on your model is initialized on time with your starting page and changed on navigation actions. We can easily keep partial HTML pieces as separate files in our source, by placing them in the webapp/partials directory of our Maven project, and reference them using a portlet resource URL. Constructing such a resource URL can be done using the liferay-portlet-url Javascript service that Liferay provides.

var resourceURL = Liferay.PortletURL.createRenderURL();
resourceURL.setPortletId(pid);
resourceURL.setPortletMode('view');
resourceURL.setWindowState('exclusive');
resourceURL.setParameter('jspPage', '/partials/list.html');

$scope.page = resourceURL.toString();

This code can be easily moved to an AngularJS service and switching pages is as simple as calling a function using the ng-click attribute, calling the service with the correct parameters and assigning the result to the page field on the model. You can find the complete source code for this in the example portlet on GitHub.

To make sure this Liferay service is loaded and available you also need to add something to the aui:script tag in your view.jsp
<aui:script use="liferay-portlet-url,aui-base">
 

No REST for the wicked

Now that we are able to have multiple AngularJS portlets, with navigation, on a page, the next step is to try and work with REST (or REST like) services. These could be REST services that Liferay provides or simple services your portlet provides and that AngularJS can consume.

First we'll look at the services Liferay itself provides. A subset of the services that you are accustomed to using in your Java based portlets are also available by default as REST services. These can be called using the Liferay.Service Javascript call and can be explored by using a simple app that Liferay exposes: check out /api/jsonws on a running Liferay instance. With this app you can easily explore the services, how they can be called and which parameters you'll need to provide. Calling such a service, in our case the Bookmark service, is pretty easy:
Liferay.Service(
    '/bookmarksentry/get-group-entries',
    {
        groupId: Liferay.ThemeDisplay.getScopeGroupId(),
        start: -1,
        end: -1
    },
    function(obj) {
        // do something with result object
    }
);
This code too can be easily moved to an AngularJS service as is shown in the example portlet on GitHub. You'll also notice the use of the Liferay.ThemeDisplay Javascript object here. It provides us with access to most of the stuff you're accustomed to using via the ThemeDisplay object in normal Liferay java code such as the company ID, group ID, language, etc... .
 
As with the resource URL stuff before we'll again have to make sure the Liferay.Service Javascript stuff is loaded and available by adding liferay-service to the use attribute of the aui:script tag in our view.jsp
<aui:script use="liferay-portlet-url,liferay-service,aui-base">
If you need to access Liferay services that aren't exposed by Liferay as a REST service or if you just want to expose your own data I'll show you a simple method here that can be used in a MVCPortlet. We'll need to implement the serveResource method in our portlet so that we can create a ResourceURL for it and use that in our AngularJS code.
public class AngularPortlet extends MVCPortlet {
     
    public void serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse) throws IOException, PortletException {

        String resourceId = resourceRequest.getResourceID();    

        try {
            // check resourceID to see what code to execute, possibly using a parameter and return a JSON result

            String paramValue = resourceRequest.getParameter("paramName");         

            Gson gson = new Gson();
            String json = gson.toJson(result);

            resourceResponse.getWriter().print(json);
        } catch (Exception e) {
            LOGGER.error("Problem calling resource serving method for '" + resourceId + "'", e);
            throw new PortletException(e);
        }
    }
}
In the actual example portlet on GitHub you'll see I've implemented this a bit differently using a BasePortlet class and a custom annotation so that you're able to annotate a normal method to signal to which resourceId it should react, but the example code above should give you the general idea. Once you have a serveResource method in place you can call it from within your AngularJS code as follows:
var url = Liferay.PortletURL.createResourceURL();
url.setResourceId('myResourceId');
url.setPortletId(pid);
url.setParameter("paramName", "paramValue");

$http.get(url.toString()).success(function(data, status, headers, config) {
    // do something with the result data
});
To create a valid resourceUrl that'll trigger the serveResource method in your portlet you always need to provide it with a resourceId and portletId. Additionally you also have the option of adding parameters to the call to use in your Java code.
 

You promise?

When trying out these various REST services I quickly ran into problems regarding the asynchronous nature of Javascript and AngularJS. This is something that Primefaces has shielded me from most of the time, but that immediately turned out to be something that I needed to think about and apply rigorously in an AngularJS based portlet. Luckily this is a known 'problem' that has an elegant solution: promises. This means you just need to write and call your AngularJS service/factory in a certain way and all the necessary async magic will happen. For this we'll revisit the code we used to get bookmarks, this time also wrapped in a nice AngularJS factory in a separate Javascript file:

'use strict';

angular.module("app.factories", []).

factory('bookmarkFactory', function($q) {
    var getBookmarks = function() {

        var deferred = $q.defer();

        Liferay.Service(
            '/bookmarksentry/get-group-entries',
            {
                groupId: Liferay.ThemeDisplay.getScopeGroupId(),
                start: -1,
                end: -1
            },

            function(obj) {
                deferred.resolve(obj);
            }
        );

        return deferred.promise;
    };

    return {
        getBookmarks: getBookmarks
    };
});

Using this pattern, creating a deferred result and returning a promise to it from our method, we've made are call asynchronous. Now we just need to call it correctly from our controller using the then syntax:

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

module.controller("MainCtrl", ['$scope', 'bookmarkFactory',

    function($scope, bookmarkFactory) {
        bookmarkFactory.getBookmarks().then(function(data) {
            $scope.model.bookmarks = data;
        });
    }]
);
 

Lost in translation

Now that the most important points have been tackled I wanted to move on to something that is pretty important in the country where I'm from: i18n. In Belgium we have 3 official languages, Dutch, French & German, and usually English is also thrown into the mix for good measure. So I wanted to find out to add i18n to my AngularJS portlet and while doing so see if I could make it into a custom directive. The current version of this simple directive uses an attribute and only allows retrieving a key without providing and replacing parameters in the value.

module.directive('i18n', function() {
    return {
        restrict: 'A',
        link: function(scope, element, attributes) {
            var message = Liferay.Language.get(attributes["i18n"]);
            element.html(message);
        }
    }
});

The directive above uses the Liferay.Language Javascript module to retrieve the value of the given resource key from the portlets language bundles and sets this as the value of the tag on which the directive is used. To be able to use this Liferay Javascript module we'll again need to add something to the use attribute of the aui:script tag to make sure it is loaded and available: liferay-language. Once we have this directive using it in our HTML partials is pretty simple:

<h2 i18n="title"></h2>

This piece of HTML, containing our custom i18n directive, will try to retrieve the value of the title key from the portlet's resource bundles that are defined in the portlet.xml. The important word in the previous sentence is try, because it seems there's a couple of bugs in Liferay (LPS-16513 & LPS-14664) which causes the Liferay.Language Javascript module to not use the portlet's resource bundles, but only the global ones. Luckily there is a simple hack to will allow us to still make this work: add a liferay-hook.xml file to the portlet and use it to configure Liferay to extend its own language bundles with our portlet's.

<?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>
    <language-properties>Language.properties</language-properties>
</hook>
 

Can you hear me?

After taking all the previous hurdles you end up with a pretty usable portlet already, but the last thing I wanted to try was to get AngularJS portlets to talk to each other using IPC (Inter Portlet Communication) or something similar. As AngularJS is a Javascript framework the obvious choice would be to use the Liferay Javascript event system: Liferay.on & Liferay.fire. Integrating this in my controller turned out to be not as straightforward as I expected, but once I threw promises and $timeout into the mix I got the result I expected at the beginning. To fire an event just use the following in your code:

Liferay.fire('reloadBookmarks', { portletId: $scope.portletId });

You can use any event name you like, reloadBookmarks in our case. or even had multiple events. We also pass in the portletId ourself as this doesn't seem to be in the event data by default anymore and it can be useful to filter out unwanted events on the portlet that is the originator of the event. This event data can be basically any Javascript/JSON object you want.

Liferay.on('reloadBookmarks', function(event) {
    if (event.portletId != $scope.portletId) {
        $timeout(function() {
            bookmarkFactory.getBookmarks().then(function(bookmarks) {
                $scope.model.bookmarks = bookmarks;
            });
        });
    }
});
 

Conclusion

It took some time and a lot of Googling and trying out stuff, but in the end I can say I got AngularJS to work pretty good in a portal environment. It is usable, but for someone that is used to spending most of his time in Java and not Javascript it was quite a challenge, especially when something fails. The error messages coming from Javascript aren't always as clear as I'd want them to be and the IDE support (code colouring, code completion, debugging, ...) isn't up to what I'm used to when writing JSF/Spring based portlets. But I think the following image expresses my feelings pretty good:

 

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

Blogs
thank you for sharing this. I personally like node.js but its a close call :-) Many javascript developers like Angular because of Google being behind it. I learned a couple of interesting things from this blog. I've recently considered writing portlets in node.js but was concerned with how difficult it might be. Your article helped me to understand that effort better.
thanks again
Thank you for the kind comments... node.js in Liferay... sounds like a nice new challenge for a future blog post!
Hey Jan,

Regarding your comment:
> the IDE support (code colouring, code completion, debugging, ...) isn't up to what I'm used to

Liferay IDE 2.2 will be released next week and if you install it along with Angularjs-eclipse plugin you can get all of that support you are used to... see here:
http://i.imgur.com/9wSrLaE.png
http://i.imgur.com/1ekMNvd.png
http://i.imgur.com/GWxUVae.png
http://i.imgur.com/2qxB1Ri.png
http://i.imgur.com/y9vgoJm.png

Once Liferay IDE 2.2 I'll post more details in a new blog post.
Hi Greg,

That looks sweet. Might have to give Eclipse/Liferay IDE another chance (bene working in IntelliJ most of the time during the last couple of years).
Very nice! Thank you for this. I'm an Angular developer new to Liferay, so I was looking to combine my old knowledge with the new Liferay stuff.
Thank you very much. I am exactly working on the same with IBM websphere Portlets

I am using ng-include directive to include partial views in IBM websphere Portlet.The code snippet is below

1.Defining render URL

<portlet:defineObjects />
<portlet:renderURL var="resourceURL" windowState="MINIMIZED" portletMode="view">
<portlet:param name="jspPage" value="/partials/list.html" />
</portlet:renderURL>

2.Main.js

<script>
var myApp = angular.module("App", []);
myApp.controller("myCtrl", function ($scope )
{
$scope.page='<%=resourceURL%>';
});
</script>

3.Home.jsp

<div id="<portlet:namespace />main" data-ng-app="App" data-ng-controller="myCtrl" >
<div ng-include src="'page'"></div>
<div>

But the partials are not included. Can anyone suggest me how to fix this?Am i missing anything?
Two things that might cause this could be that your renderURL doesn't have a portletID param set and in my tests I used the windowState 'exclusive'. Also the src attribute on your ng-include div seems to have single quotes inside of double quotes and I think only one of those is needed. The combination might cause the src to be wrongly interpreted.
1.Windowstate exclusive is not defined in portlet2.0 API
https://portals.apache.org/pluto/portlet-2.0-apidocs/javax/portlet/WindowState.html
So relavent window state "MINIMIZED" is used.

2.I checked with removing single quotes inside double quotes of ng-include div, but still no luckemoticon

3.<portlet:renderURL is generating ID and adding to URL. The output of resourceURL is ..............

dl5/d5/L2dBISEvZ0FBIS9nQSEh/pw/Z7_01H8HCS0JOVV80AI5APA2G10K0/ren/m=view/s=minimized/p=jspPage=QCPpartialsQCPlist.html/=/#Z7_01H8HCS0JOVV80AI5APA2G10K0

Any other clues?
The windowState might be Liferay specific, but in my case that make sure that I only get the content of my partial without any other portlet chrome around it, but regardless of the windowState you should always get something.

The URL generation is portal specific and I'm not really familiar with Websphere's behavior in that respect. Something you might try is to actually make a real resourceURL instead of a renderURL. In that way you should be able to get into Java portlet code of your own where you could fetch the partial from the classpath. It isn't a real solution, but could work as a workaround.

One possible cause of your problem could also be the location of your partial in your WAR. They should be accessible starting from the root. So your list.html should be located in a directory named partials that is located in the root of the WAR.
1.I tried with resourceURL ,
<portlet:resourceURL var="myResourceURL" >
<portlet:param name="jspPage" value="/partials/list.html" />
</portlet:resourceURL>

the output of this myResourceURL is ...........

dl5/d5/L2dBISEvZ0FBIS9nQSEh/pw/Z7_01H8HCS0JOVV80AI5APA2G10K0/res/c=cacheLevelPage/p=jspPage=QCPpartialsQCPlist.html/=/

2. my directory structure is
MyWAR
WEB-INF
views
home.jsp
partials
list.html

i tried with both "/partials/list.html" and "/WEB-INF/partials/list.html", but not included
For a correct resourceURL you'll need to do a bit more. You'll have to provide a resource ID (see the releaseFactory code in https://github.com/planetsizebrain/angular-portlet/blob/master/src/main/webapp/js/service.js) and you'll have to implement a serveResource method where you retrieve your partial and return its contents (see https://github.com/planetsizebrain/angular-portlet/blob/master/src/main/java/be/aca/liferay/angular/portlet/BasePortlet.java).

One strange thing about the URL results you are getting is that they seem incomplete aka without host, port, context...
[...] AngularJS is a great framework for Single Page Applications (SPAs) – it’s easy to learn and use. There’s no reason not to (and actually every reason to) create your Liferay portlets as AngularJS... [...] Read More
Nice article. I have to wonder though, after all the hoops you had to jump through to get Angular.js to run in a portal environment, where's the benefit, other than using the new Google backed hotness? It does not appear to do anything that other front-end frameworks don't do, some of which are a lot more portal friendly.
Hi Joseph,

There's indeed only a real benefit for people that are already using/know AngularJS from making normal non-portal applications. For people, like myself, that are already using an established framework (JSF in our case), there are no real reasons to switch except for the 'new (Google) hotness' one.

This is also my conclusion: while I was able to get it to work (certainly after some more research: see my second Angular post) I didn't find any reason to switch from our current JSF 2.0 + Primefaces 5 toolset (which nicely keeps us away from most Javascript weirdness).
First of all, I'd like to thank you for this post. I found it very usefull.
I have a question about the use of Liferay.Language.get in your directive.
As it is a synchronous service, it will be retarding the page load. How can we make this call asynchronous for a better UX ?

Thank you,
Paco
Hi Paco,

I got the same remark from Pier Paolo Ramon and tried to address it in my follow up post, More Angular Adventures in Liferay Land: https://www.liferay.com/web/fimez/blog/-/blogs/more-angular-adventures-in-liferay-land. In this 2nd post I used the Angular Translate module and some Java code that generates JSONs for the Liferay Language bundles to hopefully work around this synchronous problem.
Thank you. What about hooking Liferay.Language.get function to be asynchronous or using a promise in your directive? Is it a crazy idea?
That would definitely be an option, but except for overwriting the actual Liferay.Language JS file I don't think there's an easy way to hook that part of Liferay. Adding a promise might be an option, but when I took my 2nd shot at the i18n part (and also validation) I decided to try and use some readily available and widely used Angular libraries and the one I ended up with for i18n didn't have an easy way to hook back in to Liferay.Language.get and so I went a completely different way.

So if you want to try something else I'd try the promise option. Let me know what your results are!
Hello Jan,

Your article was very helpful. I am trying to get the current logged in user screen name in a liferay project that is using angular in portlets.

Specifically I am getting the user ID with ThemeDisplay.getUserId(); and then trying to use Liferay.Service.Portal.User.getUserById as shown in an AUI script example here: https://gwofu.wordpress.com/2013/05/27/liferay-javascript-get-user-object/

My issue is that 'liferay-service' does not include Service.Portal and there doesn't seem to be anyone else with a similar issue and no documentation anywhere. Do you have any insight/thoughts on this issue?
Hi Peter i think you should try that with Liferay.Service instead:

Liferay.Service(
'/user/get-user-by-id',
{
userId: 1000
},
function(obj) {
console.log(obj);
}
);
Hi this is great but unfortunately it isn't working for me. Followed the instructions up to the simple hello world but angular throws the no module error: Uncaught Error: [$injector:nomod] http://errors.angularjs.org/1.3.9/$injector/nomod?

What I'm trying to do is to output a sample .json file within the portlet but no luck. emoticon
Hi Gyle,

I haven't encountered that error myself (yet), but have noticed that different versions of Liferay + Angular behave differently. What version of Liferay are you using? If you can provide me your code I'd be happy to take a look at it and try it out myself?
Hi Jan! Thanks for the response.
I'm using liferay 6.2 ee sp13 and angularjs 1.3.9. Tried loading the js files via liferay-portlet.xml and as plain <script src> inside the jsp but to no avail. Here are my codes:

----main.js---
function bootstrap(id, portletId) {
var app = angular.module(id);

/* services */
app.factory("services", ['$http', function($http) {
var serviceBase = '/data/local.json';
var obj = {};
obj.getData = function(){
return $http.get(serviceBase)
}
}]);

/*controllers*/
app.controller('listCtrl', function ($scope, services) {
services.getData ().then(function(data){
$scope.alldata = data.data;
});
});
angular.bootstrap(document.getElementById(id),[id]);
}

----view.jsp----
<div id="<portlet:namespace />app" class="co-app">
<table ng-controller="listCtrl" id="listCtrl">
<thead>
<tr>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="data in alldata">
<td>{{data.effectiveDateTime}}</td>
</tr>
</tbody>
</table>
</div>
<script>
bootstrap('<portlet:namespace />app', '<portlet:namespace />');
</script>
Hi, i see you have like this,
app.controller('listCtrl', function ($scope, services) {
services.getData ().then(function(data){
$scope.alldata = data.data;
});
});

can you try something like

app.controller('listCtrl', ['$scope', 'services' function ($scope, services) {
services.getData ().then(function(data){
$scope.alldata = data.data;
});
}]);

probably you have the liferay js minifier turned ON, so the injection gets messed up
Hi Gyle,

I added your code (and Angular version) to my Hello World sample and tested it Liferay 6.2 EE SP13, but got a different error: must return a value from $get factory method.

When I add a return to the factory and change the URL for the JSON file so it points to something inside my portlet I can get the code to work (you'll also need to turn off the Liferay minifier in your portal-ext.properties: minifier.enabled=false).

Because I tested it quickly I just put all the code inside my view.jsp: https://gist.github.com/planetsizebrain/eb222c689d2dd188a34d (my portlet's name is 'angular-hello-world-portlet' and I created a directory 'data' inside my Maven project's webapp directory and created a simple local.json file that had an array of objects with an 'effectiveDateTime' field).
cool i still see in your gist, [ .. ] missing in your controller, so that will be broken when minifier on
indeed, but I wanted to stay as close to his example as possible, but indeed with the minifier on it would need to be changed. Strangely enough I couldn't reproduce the exact error, but got a different one, which was easily fixed after which the code worked for me with the minifier off.