Skip to content
James Roper edited this page Oct 12, 2012 · 6 revisions

Adding some AJAX actions

Now that we can log in, let's start writing functionality for our application. We'll start simple, by adding dynamic functionality to the drawer, that is, the sidebar with the list of projects.

To implement the client side logic, we are going to use CoffeeScript, a language that makes Javascript very simple and easy to work with. We could just as easily use JavasSript, but Play comes with a build in CoffeeScript compiler, so we'll see how we can utilise that.

Additionally, we'll use Backbone.js to manage our views. In contrast to a typical Backbone app, where your models live on the client side, we'll keep our models all on the server side, and just use Backbone views, binding them to the views rendered by Plays scala templates. This allows us to use Plays templating system, and also means that our application will be much easier to make work in browsers that don't support Javascript, be crawled by search engines, etc.

The Project controller

Let's start with the backend. We're going to add a few new backend actions, specifically to:

  • Add a project
  • Rename a project
  • Delete a project
  • Add a group

To start, create a controller class called app/controllers/Projects.java:

package controllers;

import play.*;
import play.mvc.*;
import play.data.*;
import java.util.*;
import models.*;
import views.html.*;
import views.html.projects.*;

@Security.Authenticated(Secured.class)
public class Projects extends Controller {

}

An important thing to notice here is that we have annotated the entire class with our security authenticator. With the dashboard, we just annotated the method, but these annotations can also be placed at the class level to say that every method in this class must use this action. This can save us a lot of boiler plate code, and also saves us from accidentally forgetting to annotate a method.

Now let's add a method to create a new project:

 public static Result add() {
    Project newProject = Project.create(
        "New project",
        form().bindFromRequest().get("group"),
        request().username()
    );
    return ok(item.render(newProject));
}

We've used our existing create method on our Project model to create the new project, owned by the currently logged in user, which is returned by request().username().

Also notice that we are reusing that item template that we created earlier to render the new project. Now you'll be begin to see why we created our templates in the structure that we did earlier. This method only renders a small part of the page, that's ok, we'll be using this fragment from an AJAX action.

Let's now add a method to rename a project, but before we do, let's consider the security requirements of this function. A user should only be allowed to rename a project if they are a member of that project. Let's write a utility method in our app/controllers/Secured.java class that checks this:

public static boolean isMemberOf(Long project) {
    return Project.isMember(
        project,
        Context.current().request().username()
    );
}

You may notice here that we've used Context.current() to get the request(). This is a convenient way to get access to a request if you aren't in an action. Underneath, it uses thread locals to find the current request, response, session and so on.

Our isMemberOf method has used a new method that we haven't written on Project yet. In fact we are going to need a few new methods on the Project object, so let's open app/models/Project.java now to add them:

public static boolean isMember(Long project, String user) {
    return find.where()
        .eq("members.email", user)
        .eq("id", project)
        .findRowCount() > 0;
}

public static String rename(Long projectId, String newName) {
    Project project = find.ref(projectId);
    project.name = newName;
    project.update();
    return newName;
}

Having added a rename method to our Project model, we are now ready to implement our action in app/controllers/Projects.java:

public static Result rename(Long project) {
    if(Secured.isMemberOf(project)) {
        return ok(
            Project.rename(
                project,
                form().bindFromRequest().get("name")
            )
        );
    } else {
        return forbidden();
    }
}

Notice that first we check that the current user is a member of the project, and if they aren't, we return them a forbidden response. Also note our use of the form() method. We've seen this before, when we were populating and validating our login form. However this time, we haven't passed in a bean to decode the form into and to validate it with. Rather, we've used what's called a dynamic form. A dynamic just parses a form submission into a map of string keys to string values, and is very convenient for simple form submissions with only one or two values where you don't want to do any validation.

Let's move on to our method to delete a project:

public static Result delete(Long project) {
    if(Secured.isMemberOf(project)) {
        Project.find.ref(project).delete();
        return ok();
    } else {
        return forbidden();
    }
}

And finally, let's add a method to create a group:

public static Result addGroup() {
    return ok(
        group.render("New group", new ArrayList<Project>())
    );
}

Now that we have our controller methods implemented, let's add routes to these controllers in conf/routes:

POST    /projects                           controllers.Projects.add()
POST    /projects/groups                    controllers.Projects.addGroup()
DELETE  /projects/:project                  controllers.Projects.delete(project: Long)
PUT     /projects/:project                  controllers.Projects.rename(project: Long)

Now do a quick refresh of the application in the browser, to make sure there are no compile errors.

Javascript routes

Now we need to write some code to use our new actions. We'll be calling our code from CoffeeScript code on the client side, but before we get to doing that, Play has a nice little feature that will help us to do that. Building URLs to make AJAX calls can be quite fragile, and if you change your URL structure or parameter names at all, it can be easy to miss things when you update your Javascript code. For this reason, Play has a Javascript router, that lets us call actions on the server, from Javascript, as if we were invoking them directly.

A Javascript router needs to be generated from our code, to say what actions it should include. It can be implemented as a regular action that your client side code can download using a script tag. Alternatively Play has support for embedding the router in a template, but for now we'll just use the action method. Write a Javascript router action in app/controllers/Application.java:

public static Result javascriptRoutes() {
    response().setContentType("text/javascript");
    return ok(
        Routes.javascriptRouter("jsRoutes",
            controllers.routes.javascript.Projects.add(),
            controllers.routes.javascript.Projects.delete(),
            controllers.routes.javascript.Projects.rename(),
            controllers.routes.javascript.Projects.addGroup(),
        )
    );
}

We've set the response content type to be text/javascript, because the router will be a Javascript file. Then we've used Routes.javascriptRouter to generate the routes. The first parameter that we've passed to it is jsRoutes, this means the router will be bound to the global variable by that name, so in our Javascript/CoffeeScript code, we'll be able to access the router using that variable name. Then we've passed the list of actions that we want in the router.

Of course, we need to add a route for that in the conf/routes file:

GET     /assets/javascripts/routes          controllers.Application.javascriptRoutes()

Before we go implementing the client side code, we need to source all the dependencies that we're going to need in the main.scala.html.

CoffeeScript

Clone this wiki locally