Using Play Routes with AngularJS and RequireJS

Using Play Routes with AngularJS and RequireJS

Intro

With the appearance of JavaScript frameworks like AngularJS, it has become a lot easier to write Single-Page-Applications on the web. Play, especially in it latest incarnation 2.1 and even more so in the upcoming releases, supports this style of development with its JavaScript router, RequireJS support and the WebJars project.

So instead of continuing the templates series, I want to show you how you use AngularJS, RequireJS and WebJars with Play. For this tutorial you should already be familiar with these technologies, although it shouldn’t be hard to follow if you know at least JavaScript.

The Problem

Play has a nice router which allows type-safe routes. There is also built-in functionality to make this router available as a JavaScript object (as described in this previous blog-post). It allows you to easily make AJAX calls on your routes, for example to log in, you could call

controllers.Application.login().ajax({
  method:'POST', data:Json.stringify(credentials), success:function(data){}
})

.

Now this isn’t bad at all, but when using AngularJS, I’d prefer to use the
$http service over jQuery AJAX calls. $http offers some conveniences, for example it’s possible to automatically add an authorization header to each request, and to intercept certain requests (for example, you could install a global handler for 401 responses).

A simple solution would be to combine the two each time you want to make a request: $http.post(controllers.Application.login().url, credentials). For one, this is hard to read, and also not easy to refactor. So let’s write an AngularJS module, i.e. a service, that does this for us.

WebJars

The first step is to declare our client-side dependencies on the server using the excellent WebJars project. Makes no sense? Wait for it! Add the following dependencies to your project/Build.scala and run play update:

"org.webjars" % "angularjs" % 1.0.7,
"org.webjars" % "requirejs" % 2.1.1,
"org.webjars" % "webjars-play" % 2.1.0-1

The webjars-play plug-in comes with a RequireJS plug-in that allows us to resolve our WebJars dependencies with ease:

define("angular", ["webjars!angular.js"], function() {
  return angular;
});

This simply wraps AngularJS (retrieved by the WebJars plug-in from the depths of our Ivy repository) in a RequireJS module called angular, which we can use in all our JavaScript files with a guarantee that it has already been loaded. Great!

RequireJS modularization

We also want to use RequireJS to load the JavaScript router. This turns out to be very easy (once you know how to do it).

Create the Play action that generates the router, for example like this:

def jsRoutes(varName: String = "jsRoutes") = Action { implicit request =>
  Ok(
    Routes.javascriptRouter(varName)(
      routes.javascript.Application.login,
      routes.javascript.Users.user
    )
  ).as(JAVASCRIPT)
}

In your conf/routes files, declare the corresponding route:

GET     /jsroutes    controllers.Application.jsRoutes(varName ?= "jsRoutes")

In your main JavaScript file (app.js or main.js are typical names), define the RequireJS module:

define("jsroutes", ["/jsroutes"], function() {
  return jsRoutes;
});

This defines a module jsroutes which retrieves the contents from the URL /jsroutes and returns the global object that this script generates, called jsRoutes (the var name from earlier).

AngularJS module

Both AngularJS modules and RequireJS module are used for dependency management, but other than that, they are quite different (although this might change in future versions of Angular).

In a new file, let’s say /app/assets/javascripts/services/playRoutes.js, we will create our Angular module. By wrapping everything in define(), we also declare a RequireJS module, also called playRoutes (inferred from the filename). We use RequireJS to pull in the dependencies on AngularJS and the JavaScript router. Inside this define, we create a new AngularJS module, called play.routing, which in turn declares our brand-new service, playRoutes.

I’m using a service instead of a factory because I don’t intend to create any instances, a simple object should be sufficient.

define(["angular", "jsroutes"], function(angular, jsRoutes) {
  var module = angular.module("play.routing", []);
  module.service(playRoutes, [$http, function($http) {
    // code
  }]);
  return module;
}

Code

So much for the boilerplate.

The jsRouter is organized in wrapped objects:

{
  controller: {
    Application : {
      login: function()
    },
    Users : {
      user: function(id)
    }
  }
}

Each function, when invoked, returns a constructed url, method, and the configured ajax function.

To wrap each function with $http, our service iterates each object, down to the actual function, adds a new wrapping function to our service object, which then configures the actual invocation (e.g. post()) in our own function. The wrapper code looks like this:

var wrapWithDollarHttp = function(playFunction) {
  return function(/*arguments*/) {
    var routeObject = playFunction.apply(this, arguments);
    var httpMethod = routeObject.method.toLowerCase();
    var url = routeObject.url;
    var res = {};
    res[httpMethod] = function(obj) {
      return $http[httpMethod](url, obj);
    };
    return res;
  };
};

Again, this just takes the original function (generated from Play), and wraps it in another function. We do this because we have to use the actual parameters (the user’s credentials, for example) to evaluate the url. This is necessary because a parameter such as the user ID might become part of the path. This also gives us the HTTP method which is in turn the name of the $http function to invoke.

So we add another function named using the HTTP method, which upon invocation will call the corresponding $http method with the url parameter already applied.

Isn’t functional programming wonderful?

Usage

Finally, we can use this module, by adding play.routing to our app’s module dependencies:

var app = angular.module("app", ["play.routing"]);

and also declaring it as a dependency on our controllers (playRoutes is the name the service itself has). We can use this as we would use $http calls, returning promises for easy composition.

function LoginCtrl($scope, $location, playRoutes) {
  $scope.credentials = {};

  $scope.login = function() {
    playRoutes.controllers.Application.login().post($scope.credentials).then(function(response) {
      $location("/dashboard");
    }, function(response) {
      $scope.error = response.err;
    });
  }
};

If the route takes parameters, we must pass these to the first function:

function EditUserCtrl($routeParams, playRoutes) {
  playRoutes.controllers.Users.getUser(routeParams.id).get().then(function(response) {
    //...
  });
}

Note that I didn’t specify an error handler, because failing to access a user should be handled by a $http interceptor, one of the reasons we did all this in the first place.

Wrap Up

So this should give you a good overview on how to integrate Play with AngularJS and RequireJS.

Check out the full source code of this example on GitHub.

Happy routing!

Posted in Scala & Playframwork
4 comments on “Using Play Routes with AngularJS and RequireJS
  1. Tim Schaub says:

    Any suggestions on setting up a Play project that uses multiple AngularJS apps? In your example, there is one main.js and a single call to angular.bootstrap. I’m trying to set up a project that will have multiple views that will be different apps from the Angular perspective (sharing common services and other modules).

    I’m also curious if the approach in your example (using the WebJars require instead of the one bundled with Play) has any implications for minification/optimization. I was hoping to use require for dynamic loading of scripts during development and then have a single script in production.

    Thanks for the great writeup.

    • marius says:

      I’m not sure how you want to organize your app, but so far splitting everything in modules and using a single app with a single bootstrap worked well for me. Maybe ask this on StackOverflow, or drop me a line to talk about the details.

      As for optimization, each WebJars library includes the minified version, but other than that optimization is not yet supported. However this currently under development, so hopefully only a question of time (and then it will probably support CDNs).

      Edit: I now know how to optimize with WebJars, I will blog about this soon. This should also give you a hint on how to organize the app.

  2. Thanks very much for this great post. I agree, playRoutes module is easier and concise to use. But how do we handle errors in this case? On unsuccessful login I am returning http status as 400, so in that case my control is not going to ‘.then(function())’. I noticed you have mentioned we have to handle this using $http interceptor, but I don’t know how to. Is there an example project with error handling available?
    I am fairly new to AngularJS, just one week into it now.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Time limit is exhausted. Please reload CAPTCHA.