Cabo A/S · Klosterport 4a · DK 8000 Aarhus C · Tel: +45 8819 3400 · E-mail: info@cabo.dk

Building a RESTful Ajax App With Dojo and Python Werkzeug (part 3): The front-end

Posted by Jakob at Feb 14, 2011 03:55 PM |

Third post in a series that demonstrates how to create a small RESTful Ajax application leveraging the Dojo Toolkit and Python Werkzeug.

This post describes the implementation of an Ajax front-end using the dojo toolkit. In part one of this series I described the architecture, and in part two I implemented a RESTful backend. In this post we'll utilize that back-end and produce an UI with the dojo toolkit. The application is available at letsplantheeventThe source code is available at github.

Dojo is a comprehensive versatile JavaScript toolkit which offers about anything you can think of when it comes to JavaScript web development. The example application takes advantage of the following components in the toolkit:

If you are new to dojo checkout "Dojo from the ground up" - a good introduction to many of the dojo basics.

RESTing with Dojo

Dojo includes thorough support for interacting with web services. Especially, when it comes to RESTful JSON web services, it requires a minimal amount of client-side code to interact with these; naturally, because of the predefined application protocol governed by REST.

In dojo, we interact with RESTful resources through the JsonRestStore. In the last post, we implemented tasks as RESTful resources. A transcript of a session with a JavaScript interpreter interacting with those resources can look as follows:

dojo.require("dojox.data.JsonRestStore"); 
tasks = dojox.data.JsonRestStore({target: "/tasks/"});

// create new task, and save with POST
var t = tasks.newItem({name:"a task"});
tasks.save();

// NOTE: save is async. so you need wait here for 
// the save to finished (or use a deferred)
t.__id; // "/tasks/1" <- from the location header in the XHR request.

// edit task and PUT it back.
tasks.changing(t);
t.name = "the task";
tasks.save();

// GET all tasks
tasks.fetch();

// GET item with specific id
tasks.fetchItemByIdentity({identity:t.__id});

// DELETE item
tasks.deleteItem(t);
You can tryout the example by pointing your browser to letsplantheevent, and paste code into your JavaScript console.

Transforming data into information with Dojo

Dojo includes many different layout widgets. The cool thing with these widgets are that they support the different dojo datastores, which all have the same uniform interface. That uniform interface makes it possible to seamlessly wire together data and widgets.

Tasks Data Grid
Tasks data grid

For the example application, we use the data grid, which enables users to explore and edit data quickly. The grid is depicted in the image, and is defined programmatically by:

dojo.require("dojox.grid.DataGrid");
tasks_grid = new dojox.grid.DataGrid({
  name: "tasks",
  structure: tasks_grid_structure, 
  store:tasks}, 'tasks_grid');

tasks_grid.startup();
In the code, the 'tasks_grid' refers to the id of the DOM node where the grid is injected - a standard dijit widget pattern. The structure property defines the layout of the grid, manifested in an array of views. In views, we take advantage of an undocumented field: type. type refers to the type of widget that presents the value in the grid. constraint is a field used by our formatter, to convert ISO 8601 datetimes to a pretty-printed string. The structure of the grid is defined as follows:
  
dojo.registerModulePath("lpte", "/js/lpte");
dojo.require("lpte.formatters");

dojo.require("dojox.grid.cells.dijit");
var tasks_grid_structure = [
    { 
      field: 'name',
      editable: true
    },
    {
      field: 'startdate',
      width: '70px',
      editable: true,
      type: dojox.grid.cells.DateTextBox,
      constraint: {formatLength: 'long', selector: "date"},
      formatter: lpte.formatters.datetimeFormatter
    },
    {
      field: 'starttime',
      width: '70px',
      editable: true,
      type: dojox.grid.cells.TimeTextBox,
      constraint: {timePattern: "HH:mm", selector: "time"},
      formatter: lpte.formatters.datetimeFormatter
    },
    // omitted end(time|date) for brevity
    {
      name: 'Actions',
      field: 'id',
      formatter: function(id, rowIdx, cell){
        if(!id){
          return "<em>unsaved</em>";
        }
        var delete_action = "tasks.deleteById('" + id + "');";
        return '<button onclick="' + delete_action + '">delete</button>';
      }
    }
  ];

  tasks_grid = new dojox.grid.DataGrid({
    name: "tasks",
    structure: tasks_grid_structure, 
    store:tasks}, 'tasks_grid');

  tasks_grid.startup();

The actions column shows a delete button when tasks have an id. On press the button calls the function tasks.deleteById, which is a function mixed into the tasks store.

dojo.require('dojox.data.JsonRestStore');

tasks = new dojox.data.JsonRestStore({
  target:"/tasks/", 
  deleteById: function(id){
    this.fetchItemByIdentity({identity: id, onItem: function(item){
      tasks.deleteItem(item);
      tasks.save();
    }});
  }
});

Left is the save button and new button. The code for the save button looks as follows:


var save_tasks_button = new dijit.form.Button({
    label: "Save",
    onClick: function(){
      tasks.save({onComplete: function(){
        // disable save button on saves.
        save_tasks_button.set('disabled', true);
      }});
    },
    disabled: true                                                 
  }, 'save_tasks_button');

  // enable save button when new item is added
  dojo.connect(tasks, "onNew",  function(event){
    save_tasks_button.set('disabled', false);
  });

  // enable save button when some property is set
  dojo.connect(tasks, "onSet",  function(event){
    save_tasks_button.set('disabled', false);
  });

There is one caveat in the above: for some reason the TimeTextBox widget is not availble in dojox.grid.cells! Most likely, due to some oversigt. We define it ourself:


  dojo.declare("dojox.grid.cells.TimeTextBox", dojox.grid.cells._Widget, {
    widgetClass: dijit.form.TimeTextBox,
    setValue: function(inRowIndex, inValue){
      if(this.widget){
        this.widget.set('value', new Date(inValue));
      }else{
        this.inherited(arguments);
      }
    },
    getWidgetProps: function(inDatum){
      return dojo.mixin(this.inherited(arguments), {
        value: new Date(inDatum)
      });
    }
  });

And that's it! We now have an advanced RESTful Ajax web service client. In the next post I'll take things up a notch and make the tasks grid a live collaborative grid using Comet updates. 

Document Actions
comments powered by Disqus