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?