Zen and the art of...

2009-12-03

Closure Library Tutorial: Tasks (part 1)

Today we'll create something with the recently released Closure Library. Nothing amazing, just a simple application to manage daily tasks (I would have liked to say a 'task manager' but that could be confusing). JavaScript has finally matured and with this library, we nearly have all the bells and whistles of a desktop environment graphical user interface.

The Closure Library is part of a set of tools that Google's engineers have been working on for quite some time and they have more than proved their worth. Even though not directly based on it, this library has been greatly influenced by the Dojo Toolkit. I've never used that framework before, most of my experience being with jQuery, so I'm not really qualified to compare them, learning Closure will be enough for now. Lets list some of the features offered by this library:

  • History management
  • Solid and portable event handling (with a timer class and ways to delay or throttle events)
  • Internationalization
  • Basic support for spell checkers
  • A complete debugging and testing framework
  • DOM manipulation helpers
  • Code to help working with data sets
  • Cross-browser support for drawing using SVG, VML or the new HTML5 canvas element
  • A module system to dynamically load compiled JavaScript code
  • UI widgets and effects

And there's much more! All that backed up by the company that dominate the web, how could we ask for more? So in this tutorial we'll concentrate on two areas: UI widgets and event handling. I know, this is already covered in the Google code tutorial, but I found it to be plain and boring, that library deserves more.

This tutorial will build upon the official one, just refer to it if you're lost at some point. We begin by declaring the namespaces we'll be providing and we'll also require the goog.dom namespace and the UI widgets we need.

goog.provide('mu.tutorial.tasks');
goog.provide('mu.tutorial.tasks.Task');

goog.require('goog.dom');
goog.require('goog.ui.CustomButton');
goog.require('goog.ui.Toolbar');
goog.require('goog.ui.ToolbarButton');
goog.require('goog.ui.Zippy');

First thing to note is that we have to provide not only the namespace, but the name of all classes in that namespace. That's because classes aren't really classes, they're still just plain JaveScript prototypes. So class names are simply namespaces and are thus orthogonal to the inheritance mechanism. You can inherit from classes that aren't being provided by any namespace. The provide and require functions are only there to check for dependencies and are also used by the debug resolver.

Now lets add something new, a toolbar. Before delving into code, we'll have to go fetch the stylesheet and images that can be found in the goog/demos folder. In the meantime you can also get those for the button widget that we'll use later on. Another file not to forget is the common.css stylesheet that contains styles common to all widgets. Here's all the code needed to create our toolbar.

mu.tutorial.tasks.switchPanel = function(target) {
    return function() {
        goog.dom.$('taskList').style.display = "none";
        goog.dom.$('completedTaskList').style.display = "none";
        goog.dom.$('deletedTaskList').style.display = "none";
        goog.dom.$('settings').style.display = "none";
        goog.dom.$(target).style.display = "block";
    }
}

mu.tutorial.tasks.attachToolbarButton = function(toolbar, label, tooltip, target) {
    var button = new goog.ui.ToolbarButton(label);
    button.setTooltip(tooltip);
    toolbar.addChild(button, true);
    goog.events.listen(button.getContentElement(), goog.events.EventType.CLICK,
        mu.tutorial.tasks.switchPanel(target));
}

mu.tutorial.tasks.attachToolbar = function(container) {
    var toolbar = new goog.ui.Toolbar();

    mu.tutorial.tasks.attachToolbarButton(toolbar, 'Tasks', 'List currently active tasks.', 'taskList');
    mu.tutorial.tasks.attachToolbarButton(toolbar, 'Completed', 'List completed tasks.', 'completedTaskList');
    mu.tutorial.tasks.attachToolbarButton(toolbar, 'Trash', 'List deleted tasks.', 'deletedTaskList');
    mu.tutorial.tasks.attachToolbarButton(toolbar, 'Settings', 'Settings', 'settings');

    toolbar.render(container);
}

First, we create a toolbar, then attach all buttons and finally render everything in the given container. There's a function to help us attach buttons, it add a tooltip and an event listener to switch between the various panels. Those panels are just a bunch of divs that are shown successively by changing their display attribute, using the switchPanel function.

For an application managing tasks, we better create a Task class. This part of the tutorial is very similar to Google's one. Task objects are structurally identical to Note objects, with summary instead title and description instead of content as properties.

mu.tutorial.tasks.Task = function(data, container) {
    this.summary = data.summary;
    this.description = data.description;
    this.priority = data.priority;
    this.parent = container;
}

mu.tutorial.tasks.Task.prototype.closeEditor = function() {
  this.contentElement.innerHTML = this.description;
  this.contentElement.style.display = "block";
  this.editorContainer.style.display = "none";
};

mu.tutorial.tasks.Task.prototype.openEditor = function(e) {
  this.editorElement.value = this.description;
  this.contentElement.style.display = "none";
  this.editorContainer.style.display = "inline";
};

mu.tutorial.tasks.Task.prototype.save = function(e) {
  this.description = this.editorElement.value;
  this.closeEditor();
};

As you see, there's nothing new here. I've also included the functions that will be used by the editor, then again, same thing as the official tutorial. We'll only add one new kind of action. In fact this is a function returning a closure that will move a task to the desired panel.

mu.tutorial.tasks.Task.prototype.clickActionButton = function(task, element, target) {
    return function(e) {
        var parent = element.parentNode;
        parent.removeChild(element);
        if (parent.childNodes.length == 0)
            mu.tutorial.tasks.noTasks(parent);

        task.parent = mu.tutorial.tasks.taskLists[target];
        if (task.parent.childNodes[0].className == 'empty')
            task.parent.removeChild(task.parent.childNodes[0]);

        task.makeDom();
        e.stopPropagation();
    };
}

Here, the noTasks function only create an h2 header tag to tell the user there's no tasks in that panel. The taskLists namespace variable is a simple dictionary that we'll fill later.

We're now ready to jump to the makeDom function, where we create the DOM structure for a task, add buttons, wire up event listeners and use the Zippy class.

mu.tutorial.tasks.Task.prototype.makeDom = function() {
    this.summaryDiv = goog.dom.createDom('div', { 'class': 'summary' }, this.summary);
    this.contentElement = goog.dom.createDom('div', { 'class': 'description' }, this.description);
    this.editorElement = goog.dom.createDom('textarea', null, '');

    this.editorContainer = goog.dom.createDom('div', {'style': 'display:none;'},
                                              this.editorElement);

    this.descriptionContainer = goog.dom.createDom('div', null,
                                                   this.contentElement,
                                                   this.editorContainer);

    var taskDiv = goog.dom.createDom('div', { 'class': 'task' },
                                     this.summaryDiv,
                                     this.descriptionContainer);

    this.parent.appendChild(taskDiv);

    this.makeButtons(this, taskDiv);

    goog.events.listen(this.contentElement, goog.events.EventType.CLICK,
                       this.openEditor, false, this);

    this.zippy = new goog.ui.Zippy(
        this.summaryDiv,
        this.descriptionContainer);
}

There is two type of button for tasks, action buttons that will use the clickActionButton function and editor buttons for the editor. Each type of button has a function to create them, add a class name, render them and add an event listener.

mu.tutorial.tasks.Task.prototype.makeActionButton = function(task, element, target, name) {
    var button = new goog.ui.CustomButton(name);

    button.addClassName('taskButton');

    button.render(element.childNodes[0]);

    goog.events.listen(
        button.getContentElement(),
        goog.events.EventType.CLICK,
        this.clickActionButton(task, element, target));
}

mu.tutorial.tasks.Task.prototype.makeEditorButton = function(element, name, callback) {
    var button = new goog.ui.CustomButton(name);

    button.addClassName('editorButton');

    button.render(element.childNodes[1].childNodes[1]);

    goog.events.listen(
        button.getContentElement(),
        goog.events.EventType.CLICK,
        callback, false, this);
}

Here's the makeButtons function.

mu.tutorial.tasks.Task.prototype.makeButtons = function(task, element) {
    if (task.parent.id == 'taskList') {
        this.makeActionButton(task, element, 'completed', 'Done');
        this.makeActionButton(task, element, 'deleted', 'Delete');
    }
    else
        this.makeActionButton(task, element, 'active', 'Undo');

    this.makeEditorButton(element, 'Save', this.save);
    this.makeEditorButton(element, 'Cancel', this.closeEditor);
}

The action buttons are attached to each tasks depending if the given task is active or not. Active tasks can be deleted or marked as completed and we can reactivate them after that. All tasks will remain editable for now, so we add the editor buttons to each one of them.

Finally, the last function takes a bunch of raw task data, create Task objects and call their makeDom method, inserting them into the given container. It also fills the taskLists dictionary entry for that list.

mu.tutorial.tasks.makeTasks = function(name, data, container) {
    if (data.length == 0)
        mu.tutorial.tasks.noTasks(container);
    else
        for (var i = 0; i < data.length; i++) {
            var task = new mu.tutorial.tasks.Task(data[i], container);
            task.makeDom();
        }

    mu.tutorial.tasks.taskLists[name] = container;
}

We still have to make use of all that code, an HTML page will contain the remaining parts. We only need one div for the toolbar, then four others for each sections of our application. After that we add some more JavaScript to attach the toolbar and create some tasks to test if everything is working.

  <body>
    <h1>Closure Library Tutorial</h1>
    <div id="menu"></div>
    <div id="taskList"></div>
    <div id="completedTaskList"></div>
    <div id="deletedTaskList"></div>
    <div id="settings">TODO...</div>
    <script type="text/javascript">
      function main() {

          var menu = document.getElementById('menu');
          mu.tutorial.tasks.attachToolbar(menu);

          var makeTaskData = function(summary, description, priority) {
              return { 'summary': summary, 'description': description, 'priority': priority };
          };

          var taskList = document.getElementById('taskList');
          mu.tutorial.tasks.makeTasks('active', [
              makeTaskData("Make the toolbar actually do something.", "Need more studying of Toolbar demo.", 99),
              makeTaskData("Make tasks editable.", "Like in the Closure Library Notes tutorial.", 88),
              makeTaskData("Create different task lists.", "One for active tasks, another for completed ones and finally a list containing deleted tasks.", 100)
              ], taskList);

          var completedTaskList = document.getElementById('completedTaskList');
          mu.tutorial.tasks.makeTasks('completed', [], completedTaskList);

          var deletedTaskList = document.getElementById('deletedTaskList');
          mu.tutorial.tasks.makeTasks('deleted', [
              { 'summary': 'test', 'description': 'test', 'priority': 1 }
              ], deletedTaskList);
      }
      main();
    </script>
  </body>

You can grab the full HTML file here (look at the source or use "Save Link As...") and the JavaScript used there. When testing it, you'll notice the loading time can be long for a static page. Using Firebug net panel, we can see that the page ask for no less than 54 JavaScript files, that's a lot of requests! Over the wire, this can quickly become a problem, especially for high latency connections. To fix this problem, we'll use the calcdeps.py script found in the bin directory. You simply have to call it like that:

> ./closure/bin/calcdeps.py -i tasks.js -p . -o script > tasks.nodeps.js

Still, it makes our little JS file a lot heavier, it now weight in at 656KB, 650 more than the original. That's a lot, but can be mitigated by server settings like file compression and expire headers.

Here's a demo if you're not interested by setting up everything yourself. In the next tutorial we'll learn to use sliders and color pickers to make the settings panel do something and we'll make the active task list sortable by priority.

Any questions, corrections or insults?

1 comment:

  1. I love closure. Great article, thanks for the hard work.

    ReplyDelete

About Me

My photo
Quebec, Canada
Your humble servant.