Nowadays, single page HTML5 apps are becoming more and more common. In this world, there is a JavaScript framework developed by Google that caught our attention. In fact, there is no surprise here: at the last Devoxx conference, AngularJS was pretty much everywhere!

Here at Nuxeo we tried to use it for an internal application and in this post, I will explain how we did it and what choices we made.

A Development Workflow is Required

One of the strengths of AngularJS is its capacity to be tested. What we would like to find is a way to have the same Test Driven Development workflow when we develop in JavaScript. The good news is that some people already made some tools for this:

  • Jasmine for testing
  • Karma to automate tests
  • Bower to fetch web packages
  • Grunt to run tasks (like ant or make)

One team at Google made a very cool tool that assembles all these called Yeoman. This tool offers a workflow and an opinionated way of doing things. Yeoman provides application templates and a CLI to make scaffolding.

After having installed Yeoman, run the following commands. We will use all defaults except that we won’t use Bootstrap with Compass. (Bootstrap is a CSS framework that can compile with scss files, but we will prefer LESS since it is its original form.)

[email protected] ~/src/angular-blog$ npm install -g generator-angular
[email protected] ~/src/angular-blog$ yo angular
 Would you like to include Twitter Bootstrap? (Y/n)
 If so, would you like to use Twitter Bootstrap for Compass (as opposed to vanilla CSS)? (Y/n) n
 Would you like to include angular-resource.js? (Y/n)
 Would you like to include angular-cookies.js? (Y/n)
 Would you like to include angular-sanitize.js? (Y/n)</em>

That’s it! Now you can run your HTML app by running:

grunt test
grunt server

It will open your browser with a simple web page saying that everything is running!

There are a lot of things that are happening under the hood here. For instance, try to open the file app/views/main.html and make some changes in the HTML: every time you will save the file, the page will be refreshed automatically to reflect your changes (it’s based on livereload).

Binding With Content Automation

The way to access Nuxeo data from the outside is by using Content Automation. For the purpose of this example, we will try to fetch all entries from the continent vocabulary and show it into the UI.

As I said before, we want a TDD coding style, so the first thing we will do is create a test. We will edit the file test/spec/services/directory.coffee.

describe 'Directory', ->

  $httpBackend = undefined
  continents = [
    {"id":"europe","label":"label.directories.continent.europe"},
    {"id":"africa","label":"label.directories.continent.africa"},
    {"id":"north-america","label":"label.directories.continent.north-america"},
    {"id":"south-america","label":"label.directories.continent.south-america"},
    {"id":"asia","label":"label.directories.continent.asia"},
    {"id":"oceania","label":"label.directories.continent.oceania"},
    {"id":"antarctica","label":"label.directories.continent.antarctica"}
  ]

  describe '#query a directory', ->

    beforeEach ->
      angular.module("test", ["services.nuxeo"]).constant "NUXEO_CONFIG", nuxeo_config =
        contextPath: "/nuxeo"
      module "test"

    beforeEach module('services.nuxeo')
    beforeEach inject(($httpBackend,NUXEO_CONFIG) ->
      @httpBackend = $httpBackend

      $httpBackend.when('POST',[NUXEO_CONFIG.contextPath,'/site/automation/Directory.Entries'].join(""),
        '{"params":{"directoryName":"continent"}}').respond 200, continents

    )

    afterEach ->
      @httpBackend.verifyNoOutstandingExpectation()
      @httpBackend.verifyNoOutstandingRequest()

    it 'should be able to retrieve continents', inject((NuxeoDirectory, $httpBackend) ->
      resolved = false
      promise = NuxeoDirectory("continent").query("blobs")
      expect(promise).not.toBe undefined
      conts = promise.then( (conts)->
        expect(conts.length).toBe 7
        expect(conts[0].id).toBe "europe"
        resolved = true
      )
      $httpBackend.flush()
      expect(resolved).toBe true

    )

This code is written in CoffeeScript. It’s not mandatory for AngularJS, but I find that it gives a clearer view of the code. Yeoman takes care of compiling this code in JavaScript every time it is modified. The compiled file will be in the tmp folder of our app: .tmp/spec/services/directory.js.

This test just verifies that when we make a call to the NuxeoDirectory service, it makes an HTTP request to the /nuxeo/site/automation/Directory.Entries automation endpoint. All the mocking stuff is well documented in AngularJS documentation.

In order to execute this test, we will have to make a small change in the Karma configuration. Karma is the tool that is used to execute our Jasmine test. Its configuration file can be found here: karma.conf.js. In order to add our compiled file to tests we will change the files definition:

files = [
  JASMINE,
  JASMINE_ADAPTER,
  'app/components/angular/angular.js',
  'app/components/angular-mocks/angular-mocks.js',
  'app/scripts/*.js',
  'app/scripts/**/*.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js',
  '.tmp/spec/**/*.js'  //In order to execute compiled coffee tests
];

If we run grunt test now, it will of course fail. We now need to implement our service. This will be done through the app/services/nuxeo.coffee file. You can find the entire file on GitHub and I will only comment on some lines.

In fact the NuxeoDirectory service is just a pre-configured call to the NuxeoAutomation service:

.factory("NuxeoDirectory", ['NuxeoAutomation', (NuxeoAutomation) ->
  NuxeoDirectory = (dirName) ->
    Directory  = {}

    Directory.query = ->
      NuxeoAutomation("Directory.Entries", {directoryName: dirName}).query("blobs")

    Directory
])

The automation service is quite the same as angular-resource

  # First we define the request that is a common Automation request
  request =
    method: 'POST',
    url: url,
    headers:
      'Content-Type':'application/json+nxrequest'
    data:
      params: params

  # We call the request and return a promise
  $http(request).then((response) ->
    data = response.data
    if data == "null"
      return null

    # Depending on the datatype (list of objects or object)
    # we return what is returned by the server
    if data
      switch returnType
        when "documents","blobs"
          angular.forEach data, (item) ->
            value.push(new Resource(item));
        else
          angular.copy(data,value)  

    return value
  , (response) ->
    $q.reject(response.data.cause.cause.message)
  )

Now we can use our service in every controller like this:

.controller('MainCtrl', ['$scope','NuxeoDirectory', function ($scope, NuxeoDirectory) {
  $scope.continents = NuxeoDirectory("continent").query()
}]);

Note that the result is a promise, so it will not immediately have its content populated. If you use it in you JavaScript code, you’ll have to use the then() construct. Fortunately, the template bindings know how to deal with promises, and you can use it directly like this:

<h3>List of continents</h3>
<ul>
  <li ng-repeat="continent in continents">{{continent.id}}</li>
</ul>

The complete source code of this project is available on the angular-nuxeo-blog GitHub project.

Conclusion

In this post, we have seen how to call a Content Automation operation from an AngularJS application. Of course you have access to all operations that are exposed by the automation server. By developing your own operation, you can quickly expose your business logic. The problem here is that we are only using the POST HTTP verb and we are not doing true REST development. We have some work pending to expose a true REST API on top of Nuxeo, exposing resources and then operations on them.