Dojo Toolkit для начинающих
// JavaScript

Возможно, многие уже читали статьи из серии jQuery для начинающих, да вот с некоторых пор меня заинтересовал еще один JavaScript фреймворк, и зовется он Dojo Toolkit.
В данной статье я постараюсь описать базовые возможности Dojo, так же буду проводить параллели с jQuery, так что не пугайтесь возникшему дежавю…
Как я уже когда-то говорил – учиться лучше на примерах, так что приступим…
Подключение
Ну, для начала Вам понадобится сам фреймворк, его вы сможете скачать с домашней страницы проекта, затем подключаем его одним из следующих способов:
Используя локальный файл:
<script type="text/javascript" src="js/dojo/dojo.js" djConfig="parseOnLoad:true, isDebug:true"></script>
Данная запись аналогична предыдущей:
<script type="text/javascript">
var djConfig = {
isDebug:true,
parseOnLoad:true
};
</script>
<script type="text/javascript" src="js/dojo/dojo.js"></script>
Dojo так же доступен на следующих хостах:
<!-- AOL --> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.3/dojo/dojo.xd.js"></script> <!-- Google --> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.3/dojo/dojo.xd.js"></script>
Селекторы
Перейдем к поиску элементов, для этой цели есть следующие функции:
// самый простой вариант - получить елемент по его Id
var element = dojo.byId('elementId');
// так же можем выбрать несколько элементов, используя селекторы из CSS3, - возвращается объект NodeList
var elements = dojo.query('.elementsClass')
Список поддерживаемых CSS селекторов можно найти в документации по функции dojo.query (docs.dojocampus.org).
События
Для работы с событиями в Dojo используются функции dojo.connect и dojo.disconnect – для добавления и удаления обработчиков соответственно, приведу простой пример:
// некая функция
function update() {
console.log('click!');
}
// и некоторый элемент
var obj = dojo.byId('someId');
// вешаем обработчик
var link = dojo.connect(obj, "onclick", null, "update");
// или
dojo.connect(obj, "onclick", "update");
// или используя анонимную функцию
dojo.connect(obj, "onclick", function update() { console.log('click!'); });
// убираем наш обработчик
dojo.disconnect(link);
Функция connect так же поддерживается NodeList’ом:
// переделаем чуть-чуть предыдущий пример
dojo.query('.someClass').connect("onclick", function update() { console.log('click!'); });
Примечание: dojo одинаково понимает события click и onclick
Простые примеры
Теперь перейдем непосредственно к работе, начнем с события, которое оповестит нас о завершении строительства DOM’а:
// создаем функцию init и вызваем ее по событию OnLoad
var init = function(){
console.log("DOM построен...");
};
dojo.addOnLoad(init);
// и/или используем анонимную функцию
dojo.addOnLoad(function(){
console.log("...спасибо");
});
Тривиальная задача – организация “зебры” из некой таблицы:
// таблица c "id=tabular_data" берем ее "tbody"
// (используем селектор ">" дабы выбрать лишь нужные элементы, исключая подтаблицы)
// и каждому нечетному элементу "tr" добавляем класс "odd"
dojo.query("#tabular_data > tbody > tr:nth-child(odd)").addClass("odd");
Изменение атрибутов, классов и стилей объектов:
// получаем атрибут title
dojo.attr("Id/Node", "title"); // title="bar"
// устанавливаем атрибут title
dojo.attr("Id/Node", "title", "A new Title");
// устанавливаем несколько атрибутов
dojo.attr("Id/Node", {
"tabindex": 2, // add to tab order
"onclick": function(e) {
// add a click event to this node
}
});
// добавляем класс anewClass
dojo.addClass("Id/Node", "anewClass");
// удаляем класс anewClass
dojo.removeClass("Id/Node", "anewClass");
// переключатель класса (нет - добавит, есть - удалит)
dojo.toggleClass("Id/Node", "anewClass");
// работает так же и с NodeList (т.е. массивом элементов)
dojo.query(".selector").toggleClass("anewClass");
// вернет объект стиля
dojo.style("Id/Node");
// вернет значение opacity
dojo.style("Id/Node", "opacity");
// установит значение opacity=0.5
dojo.style("Id/Node", "opacity", 0.5);
// изменяем несколько свойств
dojo.style("Id/Node", {
"opacity": 0.5,
"border": "3px solid black",
"height": "300px"
});
// работает так же и с NodeList (т.е. массивом элементов)
dojo.query(".selector").style({
"opacity": 0.5,
"border": "3px solid black",
"height": "300px"
});
Примечание: если какая-либо функция требует указания node в качестве параметра объекта (см. dojo.animateProperty), значит она не сможет переварить NodeList, даже если там содержится лишь один объект, конечно, это логично, но после jQuery – немного напрягает
Выдвижная панель
Начнем с простенького примера – слайд-панель, она у нас будет двигаться вверх/вниз по клику на ссылке (см. пример)

Реализуем это следующим образом, по клику на ссылку, у нас будет переключаться её класс (между “active” и “btn-slide”), а панелька с id=”panel” будет выдвигаться/прятаться. (класс “active” изменяет позицию фонового изображения, см. CSS код).
dojo.addOnLoad(function(){
dojo.query(".btn-slide").connect("onclick",function(e){
dojo.stopEvent(e);
var panel = dojo.byId('panel');
if (dojo.style(panel, 'height') != 0) {
dojo.anim(panel, {height:0}).play();
} else {
dojo.anim(panel, {height:200}).play();
}
dojo.toggleClass(this, "active");
});
});
Магические исчезновения
Этот пример покажет, как можно красиво и легко убирать растворять элементы (см. пример):

Когда мы кликаем по картинке <img class=”delete”>, будет найден родительский элемент <div class=”pane”>, и его прозрачность будет медленно изменяться от opacity= 1.0 до opacity=0 – для этого воспользуемся функцией dojo.fadeOut:
dojo.addOnLoad(function(){
dojo.query(".pane .delete").connect("onclick",function(){
dojo.fadeOut({node:this.parentNode, duration:1000, onEnd: dojo.partial(dojo.style, this.parentNode, "display", "none")}).play();
// по завершению - прячем элемент display:none
});
});
Связанная анимация #1
Теперь пример посложнее, но он поможет Вам лучше понять Dojo. Всего несколько строк кода заставят квадраты двигаться, изменять размер и прозрачность. (см. пример):

// когда прогрузилась страница (DOM готов к манипуляциям)
dojo.addOnLoad(function(){
// привязываемся к событию click для элемента с class="down"
dojo.query(".down").connect("onclick",function(e){
e.preventDefault(); // удаляем событие по умолчанию
// бежим по всем найденым элементам с class="box"
dojo.query('.box').forEach(function(el){
dojo.anim(el,{ top: dojo.style(el,'top')+160 // наращиваем позицию top на 160px
, left: dojo.style(el,'left')+160 // наращиваем позицию left на 160px
, width: dojo.style(el,'width')+10 // наращиваем ширину на 10px
, height: dojo.style(el,'height')+10 // наращиваем высоту на 10px
, opacity: dojo.style(el,'opacity')-0.2 // уменьшаем opacity на 0.2
}, 1000).play(); // запускаем анимацию, указывая время в 1000ms = 1s
});
});
});
// привязываемся к событию click для элемента с class="up", далее все аналогично
dojo.query(".up").connect("onclick",function(e){
e.preventDefault();
dojo.query('.box').forEach(function(el){
dojo.anim(el,{ top: dojo.style(el,'top')-160
, left: dojo.style(el,'left')-160
, width: dojo.style(el,'width')-10
, height: dojo.style(el,'height')-10
, opacity: dojo.style(el,'opacity')+0.2}, 1000).play();
});
});
});
});
Примечание: в Opera 9.63 неправильно определяется первоначальное положение элементов
Связанная анимация #2
А теперь будем анимировать каждый box по отдельности. Для движения “вниз” будем использовать dojo.fx.chain – и вся анимация будет выполняться последовательно. Для движения вверх будем использовать dojo.fx.combine – анимация будет происходить параллельно (см. пример):
dojo.require("dojo.fx");
dojo.addOnLoad(function(){
// выбираем каждый элемент по отдельности
var box1 = dojo.query('.box:nth-child(1)')[0];
var box2 = dojo.query('.box:nth-child(2)')[0];
var box3 = dojo.query('.box:nth-child(3)')[0];
// вешаем обработчик на "down"
dojo.query(".down").connect("onclick",function(e){
e.preventDefault();
dojo.fx.chain(
[
dojo.animateProperty({node:box1,properties:{top: dojo.style(box1,'top')+160,duration:1000}}) // изменяем позицию первого квадрата
,dojo.animateProperty({node:box1,properties:{left: dojo.style(box1,'left')+160,duration:1000}})
,dojo.animateProperty({node:box2,properties:{top: dojo.style(box2,'top')+160,duration:1000}}) // изменяем позицию второго квадрата
,dojo.animateProperty({node:box2,properties:{left: dojo.style(box2,'left')+160,duration:1000}})
,dojo.animateProperty({node:box3,properties:{top: dojo.style(box3,'top')+160,duration:1000}}) // изменяем позицию третьего квадрата
,dojo.animateProperty({node:box3,properties:{left: dojo.style(box3,'left')+160,duration:1000}})
]
).play();
});
// вешаем обработчик на "up"
dojo.query(".up").connect("onclick",function(e){
e.preventDefault();
dojo.fx.combine(
[
dojo.animateProperty({node:box1,properties:{top: dojo.style(box1,'top')-160,duration:1000}})
,dojo.animateProperty({node:box1,properties:{left: dojo.style(box1,'left')-160,duration:1000}})
,dojo.animateProperty({node:box2,properties:{top: dojo.style(box2,'top')-160,duration:1000}})
,dojo.animateProperty({node:box2,properties:{left: dojo.style(box2,'left')-160,duration:1000}})
,dojo.animateProperty({node:box3,properties:{top: dojo.style(box3,'top')-160,duration:1000}})
,dojo.animateProperty({node:box3,properties:{left: dojo.style(box3,'left')-160,duration:1000}})
]
).play();
});
});
Примечание: функции dojo.fx.chain и dojo.fx.combine – работают с dojo.animateProperty и не понимают dojo.anim
Гармошка #1
Пример реализации “гармошки”. (см. пример)

Теперь приступим к разбору полетов:
// подключаем dojo.fx и dojo.NodeList-fx
dojo.require("dojo.fx");
dojo.require("dojo.NodeList-fx");
dojo.addOnLoad(function(){
// прячем все параграфы (можете использовать CSS, но будет не так интересно)
dojo.query(".accordion div p").style({ display:'none' });
// вешаемся на событие onclick
dojo.query(".accordion h3").connect("onclick",function(){
// получаем индекс текущего элемента в предке
var index = dojo.query(".accordion h3").indexOf(this);
// прячем все параграфы кроме текущего
dojo.forEach(dojo.query(".accordion div").query("p"), function(item, idx){
if (idx != index && dojo.style(item, 'display') != 'none') {
dojo.query(item).wipeOut().play();
}
});
// получаем необходимый нам <p>
var p = dojo.query("p", this.parentNode);
// проверяем наличие класса active
if (dojo.hasClass(this,'active')) {
dojo.removeClass(this,'active'); // удаляем класс active
p.wipeOut().play(); // прячем <p>
} else {
dojo.addClass(this,'active'); // добавляем класс active
p.wipeIn().play(); // показываем <p>
}
});
});
Приведу сразу код HTML, чтобы далеко не ходить:
<div class="accordion">
<div>
<h3>Question One Sample Text</h3>
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div>
<h3>This is Question Two</h3>
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div>
<h3>Another Questio here</h3>
<p>Lorem ipsum dolor sit amet...</p>
</div>
</div>
Примечание: наткнулся на различное поведение селектора :first-child в Dojo и jQuery в приведенном примере. Dojo не находит элемент <p>, т.к. он не является first-child’ом относительно парента, jQuery же находит, т.к. он является первым потомком <p>. Кто прав я не могу сказать точно, но браузеры считают, что таки Dojo… (селектор :nth-child в Dojo так же считает вхождения потомков иначе).
Еще брошу камень в огород Dojo – кода в jQuery значительно меньше, и структура документа проще (хотя, возможно, я просто не знаю, как получить в Dojo следующий элемент в доме, имея лишь текущий node).
Так же не совсем понятно, почему при переборе dojo.query(“.accordion div p”).forEach(…) элементы идут в обратном порядке (лишь в webkit’е правильно), при этом dojo.query(“.accordion div”).query(“p”).forEach(…) работает верно во всех браузерах.
Гармошка #2
Этот пример схож с предыдущим, лишь отличается тем, что мы указываем открытую по умолчанию панельку. (см. пример)
В CSS у нас указано для всех элементов <p> display:none. Теперь нам необходимо открыть третью панель. Для этого мы можем написать следующий код:
dojo.addOnLoad(function(){
// выбираем третий div и добавляем к заголовку класс active
dojo.query(".accordion div:nth-child(3) h3").addClass("active");
// работает лишь с webkit'ом
// dojo.query(".accordion div:not(:nth-child(3)) p").style({ display:'none' });
var els = dojo.query(".accordion div p"); // выбираем все параграфы
els.splice(2,1); // вырезаем третий (тут отсчет с нуля идет)
els.style({ display:'none' }); // все остальные скрываем
/* ... */
});
Примечание: Селектор вида div:not(:nth-child(2)) заработал лишь в Safari и Chrome. Кстати, реализовал я анимацию в данном примере иначе – использовал dojo.fx.Toggler – достаточно забавная вещь, правда я так и не понял как правильно переключаться между show() и hide() (к примеру, если мне хочется создать функцию аля slideToggle в jQuery)
Анимация для события hover #1
Данный пример поможет создать Вам очень красивую анимацию для события hover (надеюсь, Вы знаете что это?), (см. пример):

Когда Вы наводите мышкой на элемент меню (mouseover), происходит поиск следующего элемента <em> и анимируются его прозрачность и расположение:
dojo.addOnLoad(function(){
// вешаемся на событие onmouseover
dojo.query(".menu a").connect("onmouseover",function(){
// ищем em элемент и анимируем его
var em = dojo.query('em', this.parentNode);
dojo.animateProperty({ node: em[0], duration:500,
properties: {
opacity: { start: 0, end: 1 }, // прозрачность
top: { start:-85, end:-70, unit:"px" } // располложение
},
beforeBegin:function() { // перед началом анимации надо выставить правильно св-во display
em.style({display:'block'});
}
}).play();
});
// вешаемся на событие onmouseout
dojo.query(".menu a").connect("onmouseout",function(){
var em = dojo.query('em', this.parentNode);
dojo.animateProperty({ node: em[0], duration:300,
properties: {
opacity: { start: 1, end: 0 },
top: { start:-70, end: -85, unit:"px" }
},
onEnd:function() {
em.style({display:'none'});
}
}).play();
});
});
Анимация для события hover #2
Данный пример чуть-чуть посложней предыдущего примера: для формирования подсказки используется атрибут title (см. пример)

Первым делом добавим тэг <em> в каждый элемент <a>. Когда произойдет событие mouseover, мы возьмем текст из атрибута “title” и вставим его в тэг <em>:
dojo.addOnLoad(function(){
dojo.query(".menu2 a").connect("onmouseover",function(){
// создаем елемент em и закидываем его в DOM
var em = dojo.query(dojo.create('em', {innerHTML:dojo.attr(this, 'title')})).place(this.parentNode);
// анимация
dojo.animateProperty({ node: em[0], duration:500,
properties: {
opacity: { start: 0, end: 1 },
top: { start:-85, end:-70, unit:"px" }
},
beforeBegin:function() {
em.style({display:'block'});
}
}).play();
});
dojo.query(".menu2 a").connect("onmouseout",function(){
var em = dojo.query('em', this.parentNode);
dojo.animateProperty({ node: em[0], duration:300,
properties: {
opacity: { start: 1, end: 0 },
top: { start:-70, end: -85, unit:"px" }
},
onEnd:function() {
// удаляем элемент из DOM'a
em.orphan();
}
}).play();
});
});
Кликабельные блоки
Этот пример демонстрирует, как сделать кликабельным блок с текстом, а не только ссылку (см. пример):

Создадим список <ul> с классом class=”pane-list” и мы хотим сделать элементы <li> кликабельными. Для начала привяжемся к событию click для элемента “.pane-list li”; когда пользователь будет кликать по элементу списка, наша функция произведет поиск тэга <a> и сделает редирект на страницу указанную в атрибуте href.
dojo.addOnLoad(function(){
dojo.query(".pane-list li").connect("click",function(){
window.location=dojo.query("a", this).attr("href");return false;
});
});
Складывающиеся панельки
Ну, а теперь чуть-чуть скомбинируем предыдущие примеры и создадим ряд складывающихся панелек (наподобие как в Gmail организован inbox). (см. пример)
- скрываем все элементы <div class=”message_body”< после первого.
- скрываем все элементы <li< после пятого
- клик по <p class=”message_head”> – вызывает метод wipeOut/wipeIn для следующего элемента <div class=”message_body”>
- клик по <a class=”collpase_all_message”> – вызывает метод wipeOut для всех <div class=”message_body”>
- клик по <a class=”show_all_message”> – скрывает элемент, и отображает <a class=”show_recent_only”>, так же вызывается метод wipeIn для всех <li> после пятого
- клик по <a class=”show_recent_only”> – скрывает элемент, и отображает <a class=”show_all_message”>, так же вызывается метод wipeOut для всех <li> после пятого
dojo.require("dojo.fx");
dojo.require("dojo.NodeList-fx");
dojo.addOnLoad(function(){
// hide message_body after the first one
dojo.query(".message_list li:not(:first-child) .message_body").style({ display:'none' });
// hide message li after the 5th
// dojo.query(".message_list li").splice(4).style({ display:'none' }); // not equal for FF and IE
dojo.query(".message_list li:nth-child(1n+4)").style({ display:'none' });
// event on header click
dojo.query(".message_head").connect('onclick',function(){
var body = dojo.query(".message_body",this.parentNode)
if (dojo.style(body[0], 'display') != 'none') {
body.wipeOut().play();
} else {
body.wipeIn().play();
}
return false;
});
// collapse all messages
dojo.query(".collpase_all_message").connect('onclick',function(e){
e.preventDefault();
dojo.query(".message_body").wipeOut().play();
});
// show all messages
dojo.query(".show_all_message").connect('onclick',function(e){
e.preventDefault();
dojo.query(this).fadeOut().play();
dojo.style(this,'display','none');
dojo.query(".show_recent_only").style('display','block').fadeIn().play();
dojo.query(".message_list li:nth-child(1n+4)").wipeIn().play();
});
// hide old messages
dojo.query(".show_recent_only").connect('onclick',function(e){
e.preventDefault();
dojo.query(this).fadeOut().play();
dojo.style(this,'display','none');
dojo.query(".show_all_message").style('display','block').fadeIn().play();
dojo.query(".message_list li:nth-child(1n+4)").wipeOut().play();
});
});
Примечание: заметил различное поведение функции dojo.NodeList.splice в различных браузерах
Имитация Backend’a WordPress’a
Я думаю многие из читателей сталкивались с админской частью wordpress’a, точнее с редактирование комментариев. Попробуем сделать что-то подобное (см. пример):

- добавим класс “alt” к каждому чётному элементу <div class=”pane”> (данный класс изменяет цвет фона элемента)
- клик по <a class=”btn-delete”> инициирует появление сообщения (alert), так же происходит анимация фонового цвета и прозрачности (backgroundColor и opacity) для <div class=”pane”>
- клик по <a class=”btn-unapprove”> – вызывает анимацию фона у <div class=”pane”> (цвет изменяется на желтый и обратно) и добавляет класс “spam”
- клик по <a class=”btn-approve”> – вызывает анимацию фона у <div class=”pane”> (цвет изменяется на зеленый и обратно) и удаляет класс “spam”
- клик по <a class=”btn-spam”> – вызывает анимацию фона у <div class=”pane”> (цвет изменяется на красный), затем вызываем FadeOut(), по завершению анимации элемент удаляем из DOM’а
dojo.require('dojo.fx');
dojo.addOnLoad(function(){
// находим четные элементы
dojo.query(".pane:nth-child(even)").addClass("alt");
dojo.query(".pane .btn-delete").connect('onclick',function(e){
// удаляем стандартный обработчик события - чтобы не было перехода по ссылке
e.preventDefault();
alert("This comment will be deleted!");
// выбираем необходимый нам блок
var block = this.parentNode.parentNode;
// вся анимация будет происходить последовательно
var anim = dojo.fx.chain([
dojo.animateProperty({ node: block, properties: { backgroundColor: "#fbc7c7" }}),
dojo.fadeOut({ node: block})
]);
// по завершению анимации блок будет спрятан
dojo.connect(anim, "onEnd", function(){
dojo.style(block, {'display':'none'});
});
anim.play();
});
dojo.query(".pane .btn-unapprove").connect('onclick',function(e){
e.preventDefault();
var block = this.parentNode.parentNode;
dojo.fx.chain([
dojo.animateProperty({ node: block, properties: { backgroundColor: "#fff568" }}),
dojo.animateProperty({ node: block, properties: { backgroundColor: "#ffffff" }, onEnd:function() {
dojo.addClass(block, 'spam');
}})
]).play();
});
dojo.query(".pane .btn-approve").connect('onclick',function(e){
e.preventDefault();
dojo.fx.chain([
dojo.animateProperty({ node: this.parentNode.parentNode, properties: { backgroundColor: "#dafda5" }}),
dojo.animateProperty({ node: this.parentNode.parentNode, properties: { backgroundColor: "#ffffff" }, onEnd:function() {
dojo.removeClass(this.node, 'spam');
}})
]).play();
});
dojo.query(".pane .btn-spam").connect('onclick',function(e){
e.preventDefault();
dojo.fx.chain([
dojo.animateProperty({ node: this.parentNode.parentNode, properties: { backgroundColor: "#fbc7c7" }}),
dojo.fadeOut({ node: this.parentNode.parentNode, properties: { backgroundColor: "#ffffff" }, onEnd:function() {
dojo.query(this.node).orphan();
}})
]).play();
});
});
Примечение: код каждой фнкции немного различается, так – для разнообразия
Галерея изображений
Простейший пример реализации галереи, без перезагрузки страницы. (см. пример)

Для начала добавим тэг <em> в заголовки <h2>
По клику на изображения в <p class=thumbs> выполняем следующие действия:
- отменяем событие по умолчанию (это переход по ссылке)
- сохраняем значение атрибута “href” в переменной “largePath”
- сохраняем значение атрибута “title” в переменной “largeAlt”
- заменяем в элементе <img id=”largeImg”> значение атрибута “scr” и “alt” значениями из переменных “largePath” и “largeAlt”
- так же присваиваем элементу “h2 em” значение из “largeAlt”
dojo.addOnLoad(function(){
dojo.query(dojo.create('em')).place(dojo.query('h2'));
dojo.query(".thumbs a").connect("onclick",function(e){
e.preventDefault();
// получаем необходимые нам данные
var largePath = dojo.attr(this, "href");
var largeAlt = dojo.attr(this, "title");
// заменяем картинку и alt-текст
dojo.attr("largeImg", {src:largePath,alt:largeAlt});
em = dojo.query("h2 em")[0];
// изменяем описание
em.innerHTML = " (" + largeAlt + ")";
});
});
Стилизируем ссылки
Большинство нормальных браузеров легко понимают когда мы хотим добиться от них стилизации ссылок для различного типа файлов, для это цели можно использовать следующее CSS правило: a[href $='.pdf'] { … }. Но как обычно IE6 отличается умом и сообразительностью, по этой причине будем ставить ему костыли используя Dojo. (см. пример)

Для начала добавим класс для каждой ссылки, в соответствии с типом файла.
Затем выберем все элементы <a> которые не содержат ссылки на “http://anton.shevchuk.name” и не начинающиеся на “#” в “href”, затем добавим им класс “external” и устанавливаем target= “_blank”.
dojo.addOnLoad(function(){
dojo.query("a[href$=pdf]").addClass("pdf");
dojo.query("a[href$=zip]").addClass("zip");
dojo.query("a[href$=psd]").addClass("psd");
dojo.query("a:not([href*=http://anton.shevchuk.name])").filter(":not([href^=#])")
.addClass("external")
.attr({ target: "_blank" });
});
Так же Вы можете посмотреть все примеры или скачать Dojo для начинающих.
Полезные ссылки
Приведу ссылки на полезные ресурсы по теме:
- Общий взгляд на Dojo Toolkit
- Два примера создания data – таблицы на Dojo Toolkit и JQuery. Часть 1
- Dojo Campus – много полезной информации, так же есть свой Quick Start
- Dojo Quick Start Guide – SitePen, Inc.
- Introducing The Dojo Toolkit