JavaScript routing in Play 2 (Scala edition)
In my previous post, I covered using JavaScript routing in Play 2 Java applications. Here’s the Scala version. It’s pretty much a copy of the previous article, to make it independent.
One of the nicest features in Play 2, but one which doesn’t seem to be widely covered, is the JavaScript routing that can be generated by the framework to make AJAX-based client code more maintainable. Here, I’m going to cover using this feature in a Scala-based Play 2 application.
Overview
It’s common to see calls in JavaScript making AJAX requests. Frequently, a library such as jQuery is used to provide support. For example, given a route
GET /foo controllers.Application.getAll()
a GET request can be made using the shorthand $.get method.
$.get("/foo", function( data ) { // do something with the response });
A variant, in the case where parameters are required, needs a little bit more work.
GET /foo controllers.Application.get(personId: Long, taskId: Long) var personId = $('#person').val(); var taskId = $('#item').val(); $.get("/foo?person=" + personId + '&task=' + taskId, function( data ) { // do something with the response });
All this seems far removed from the easy style of Play applications, where interactions are idiomatic and typesafe. Happily, Play offers a feature called JavaScript routing. This allows us to use JS objects generated by the back-end, and so we can replace the code above with
var personId = $('#person').val(); var taskId = $('#item').val(); appRoutes.controllers.Application.get(personId, taskId).ajax({ success: function( data ) { // do something with the response } })
This will result in a GET request to /foo with personId and taskId as request parameters.
GET /foo?personId=personId&taskId=taskId
Changing the route file to use a RESTful-style URL of /foo/:personId/:taskId will result in the following call with no changes required to your JS code:
GET /foo/personId/taskId
Let’s take a look at what we need to do to achieve this.
The basics
Time to create our basic application. Using the command line, generate a new Play app
play new jsRoutingScala
Accept the default name of jsRoutingScala, and select the option for a Scala application.
_ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.5 (using Java 1.7.0_17 and Scala 2.10.0), http://www.playframework.org The new application will be created in /tmp/jsRoutingScala What is the application name? [jsRoutingScala] > Which template do you want to use for this new application? 1 - Create a simple Scala application 2 - Create a simple Java application > 1 OK, application jsRoutingScala is created. Have fun!
This will give us a basic controller called Application, and a couple of views. We still need to create a model class, so let’s do that now. In the app directory, create a package called models. In the models package, create a class called Person. Note the JSON read/write support – this will allow the controller to serialize and deserialize instances of Person between Scala and JSON.
package models import anorm._ import anorm.SqlParser._ import play.api.Play.current import play.api.db.DB import play.api.libs.json._ import anorm.~ case class Person(id: Pk[Long] = NotAssigned, name: String) object Person { val simple = { get[Pk[Long]]("person.id") ~ get[String]("person.name") map { case id~name => Person(id, name) } } def insert(person: Person) = { DB.withConnection { implicit connection => SQL( """ insert into person values ( (select next value for person_seq), {name} ) """ ).on( 'name -> person.name ).executeInsert() } match { case Some(long) => long case None => -1 } } def delete(id: Long) = { DB.withConnection { implicit connection => SQL("delete from person where id = {id}").on('id -> id).executeUpdate() } } def getAll: Seq[Person] = { DB.withConnection { implicit connection => SQL("select * from person").as(Person.simple *) } } implicit object PersonReads extends Reads[Person] { def reads(json: JsValue): JsResult[Person] = JsSuccess[Person](Person(NotAssigned, (json \ "name").as[String]), JsPath()) } implicit object PersonWrites extends Writes[Person] { def writes(person: Person) = Json.obj( "id" -> Json.toJson(person.id.get), "name" -> Json.toJson(person.name) ) } }
We’ll need a database, so edit the conf/application.conf and uncomment the following lines:
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play"
Because this example uses Anorm, we need to configure the database ourselves. In the conf folder, create a folder called evolutions that contains another folder, default. Create a file here called 1.sql, and copy the following into it:
# --- !Ups create table person ( id bigint not null, name varchar(255), constraint pk_person primary key (id)) ; create sequence person_seq; # --- !Downs SET REFERENTIAL_INTEGRITY FALSE; drop table if exists person; SET REFERENTIAL_INTEGRITY TRUE; drop sequence if exists person_seq;
We’re now ready to build our controller.
The controller
In the controllers package, open the Application object. There’s already an index method, but we don’t need to change this.
The app will allow us to create, delete and get Person instances, and so we need methods to support these operations.
def getAll() = Action { Ok(Json.toJson(Person.getAll)) } def delete(id: Long) = Action { Person.delete(id) Ok("Deleted " + id) } def create = Action { implicit request => request.body.asJson match { case None => BadRequest case Some(json: JsValue) => { val person: Person = json.as[Person] val id = Person.insert(person) Ok(Json.obj( "id" -> id, "name" -> person.name )) } } } def jsRoutes = Action { implicit request => Ok(Routes.javascriptRouter("appRoutes")( controllers.routes.javascript.Application.create, controllers.routes.javascript.Application.delete, controllers.routes.javascript.Application.getAll)) .as("text/javascript") }
These methods cover our business logic, so we can add them to the routes file
GET /person controllers.Application.getAll() DELETE /person/:id controllers.Application.delete(id: Long) POST /person controllers.Application.create()
Add JS routing support
So far, so normal. We have business logic that can be accessed via HTTP calls, but no specialised JS support. We need to add another method to the controller that specifies which routes we want to be available in the JS routing object.
// jsRoutes
This method will generate a JavaScript file that can be loaded into the client. We’ll see this when we get to the view. Because it’s something called from the client, we need to add another entry to the routes. It’s extremely important to note the placement of the route – it must precede the existing “/assets/*file” entry, otherwise that route will consume the request.
GET /assets/js/routes controllers.Application.jsRoutes() GET /assets/*file controllers.Assets.at(path="/public", file)
The view
Now we get to where the action is. The first step is to make our view aware of the JS routing code, and we do this by simply adding a script tag to views/main.scala.html
<script src="@controllers.routes.Application.jsRoutes()" type="text/javascript"></script>
We’re finally ready to use our JS routing object. The following code is an utterly basic single-page app that hooks into the get, create and delete functionality of the back-end. Copy and paste this code into your existing views/index.scala.html file, and then take a look at what it’s doing. You may notice this code is identical to that in the Java example.
@(message: String) @main("Play 2 JavaScript Routing") { <fieldset> <form> <label for="personName">Name: <input type="text" name="personName" id="personName"/> <input type="button" value="Create" id="createPerson"/> </form> </fieldset> <ul id="peopleList"> <script> var doc = $ (document); doc.ready (function() { // Delete a person doc.on ('click', '.deletePerson', function(e) { var target = $(e.target); var id = target.data('id'); appRoutes.controllers.Application.delete(id).ajax( { success : function ( data ) { target.closest('li').remove(); } }); }); // Create a new person $('#createPerson').click(function() { var personNameInput = $('#personName'); var personName = personNameInput.val(); if(personName && personName.length > 0) { var data = { 'name' : personName }; appRoutes.controllers.Application.create().ajax({ data : JSON.stringify(data), contentType : 'application/json', success : function (person) { $('#peopleList').append('<li>' + person.name + ' <a href="#" data-id="' + person.id + '" class="deletePerson">Delete</a></li>'); personNameInput.val(''); } }); } }); // Load existing data from the server appRoutes.controllers.Application.getAll().ajax({ success : function(data) { var peopleList = $('#peopleList'); $(data).each(function(index, person) { peopleList.append('<li>' + person.name + ' <a href="#" data-id="' + person.id + '" class="deletePerson">Delete</a></li>'); }); } }); }) ; </script> }
There are three lines here that will generate calls to the server, namely
- appRoutes.controllers.Application.delete(id).ajax
- appRoutes.controllers.Application.create().ajax
- appRoutes.controllers.Application.getAll().ajax
Let’s take delete as the example, since it takes a parameter.
- appRoutes
- Take a look at your Application class, and you’ll see the name appRoutes being given to the JS router. This forms the base of the namespace of the JS routing object, and means you can have multiple JS routing objects from different controllers imported into the same view because you can keep the names of each unique.
- controllers.Application
- This is the fully qualified name of the target controller. Note that it matches the FQN of the Scala object.
- delete(id)
- The method call, including parameters. This function will return an object called ajax which can be used to control the behaviour of the HTTP call.
- ajax
- This function makes the call to the server. It’s here that you add success and error callbacks, change the content-type of the request to match your back-end requirements, add PUT or POST data, etc.
Summary
That seemed like a lot of work, but most of it was creating the project. The actual JS routing represents a very small time investment, for a pretty good pay-off. It’s very easy to retrofit existing code to use this approach, and very simple to continue in this style once you experience the benefits.
Example application
- You can find the example code from this article on GitHub: https://github.com/schaloner/jsRoutingScala
Java
This article covered how to use this feature from a Scala viewpoint. In a sibling article, Java support is also demonstrated. The differences between the two are minimal, and non-existent from a client-side perspective. You can read this article here.