Создание одностраничного приложения для составления списка заданий при помощи Backbone.js
() translation by (you can also view the original English article)
Backbone.js – JavaScript-фреймворк для создания гибких веб-приложений. Он предоставляет Модели, Коллекции, События, Маршрутизатор и несколько других замечательных возможностей. В этом руководстве мы разработаем простое приложение для создания списка заданий с возможностями их добавления, редактирования и удаления. Мы также добавим возможность отметить, что задание выполнено, и поместить его в архив. Для того чтобы длина этого поста оставалась приемлемой, мы не будем добавлять никакого взаимодействия с базой данных. Все данные будут храниться на стороне клиента.
Предварительная подготовка
Ниже приводится файловая структура, которую будем использовать:
1 |
css |
2 |
└── styles.css |
3 |
js |
4 |
└── collections |
5 |
└── ToDos.js |
6 |
└── models |
7 |
└── ToDo.js |
8 |
└── vendor |
9 |
└── backbone.js |
10 |
└── jquery-1.10.2.min.js |
11 |
└── underscore.js |
12 |
└── views |
13 |
└── App.js |
14 |
└── index.html |
Назначение пары файлов очевидно, например /css/styles.css
и /index.html
. В них содержатся стилевые правила CSS и HTML-разметка. В контексте Backbone.js модель является местом для хранения наших данных. Так что наши задания списка просто будут моделями. И поскольку у нас будет больше одного задания, мы организуем их в виде коллекции. Бизнес-логика (* программный код, реализующий функциональность приложения. Здесь и далее примеч. пер.) распределяется между представлениями и главным файлом приложения, App.js
. Из зависимостей для Backbone.js обязательна только Underscore.js. Этот фреймворк также очень хорошо сочетается с jQuery, так что обе эти библиотеки добавляются в папку vendor
(* поставщик, вендор). Все, что нам теперь необходимо, – просто написать немного HTML-разметки, и мы готовы к созданию приложения.
1 |
<!doctype html>
|
2 |
<html>
|
3 |
<head>
|
4 |
<title>My TODOs</title> |
5 |
<link rel="stylesheet" type="text/css" href="css/styles.css" /> |
6 |
</head>
|
7 |
<body>
|
8 |
<div class="container"> |
9 |
<div id="menu" class="menu cf"></div> |
10 |
<h1></h1>
|
11 |
<div id="content"></div> |
12 |
</div>
|
13 |
<script src="js/vendor/jquery-1.10.2.min.js"></script> |
14 |
<script src="js/vendor/underscore.js"></script> |
15 |
<script src="js/vendor/backbone.js"></script> |
16 |
<script src="js/App.js"></script> |
17 |
<script src="js/models/ToDo.js"></script> |
18 |
<script src="js/collections/ToDos.js"></script> |
19 |
<script>
|
20 |
window.onload = function() { |
21 |
// bootstrap
|
22 |
}
|
23 |
</script>
|
24 |
</body>
|
25 |
</html>
|
Как вы видите, мы подключаем все внешние файлы JavaScript ближе к концу документа, поскольку согласно установившейся практике это необходимо выполнять в конце тега body. Также мы подготавливаем код для поэтапной загрузки приложения. В документе имеется контейнер для контента, меню и заголовка. Главное навигационное меню является статическим элементом, и мы не будем его менять. Мы заменим контент заголовка и элемента div
, расположенного ниже него.
Планирование приложения
Всегда хорошо, если у нас имеется план перед началом работы с чем-либо. В Backbone.js нет очень строгих архитектурных принципов, которых нам следует придерживаться. Это является одним из преимуществ данного фреймворка. Так что, перед тем как приступить к реализации бизнес-логики, давайте рассмотрим основополагающие принципы.
Определение пространства имен
(* пространство имен – набор правил именования, регулирующий видимость объектов в программе; именованная область видимости). Согласно установившейся практике ваш код следует помещать в его собственную область видимости (* область текста программы, где может быть использован данный идентификатор (имя переменной, именованной константы, функции и т. п.)). Регистрация глобальных переменных или функций – плохая идея. Мы создадим одну модель, одну коллекцию, маршрутизатор и несколько представлений Backbone.js. Все эти элементы должны находиться в собственной области видимости. В App.js
будет содержаться класс (* имитация класса), в котором размещается весь код.
1 |
// App.js
|
2 |
var app = (function() { |
3 |
|
4 |
var api = { |
5 |
views: {}, |
6 |
models: {}, |
7 |
collections: {}, |
8 |
content: null, |
9 |
router: null, |
10 |
todos: null, |
11 |
init: function() { |
12 |
this.content = $("#content"); |
13 |
},
|
14 |
changeContent: function(el) { |
15 |
this.content.empty().append(el); |
16 |
return this; |
17 |
},
|
18 |
title: function(str) { |
19 |
$("h1").text(str); |
20 |
return this; |
21 |
}
|
22 |
};
|
23 |
var ViewsFactory = {}; |
24 |
var Router = Backbone.Router.extend({}); |
25 |
api.router = new Router(); |
26 |
|
27 |
return api; |
28 |
|
29 |
})();
|
Выше показана типичная реализация шаблона проектирования (* обобщённое описание способа решения определённого класса задач) "Открытый модуль". В переменной api
содержится объект, возвращаемый функцией и предоставляющий доступ к публичным методам класса. Свойства views
, models
и collections
будут выступать в качестве вместилищ классов, возвращаемых Backbone.js. В content
будет содержаться объект jQuery для главного контейнера пользовательского интерфейса. Здесь у нас имеется два вспомогательных метода. При помощи одного обновляется вышеупомянутый контейнер. При помощи второго задается текст заголовка страницы. Затем мы определили модуль под названием ViewsFactory
. В нем будут создаваться наши представления, и в конце мы создали маршрутизатор.
Вы можете поинтересоваться, зачем нам нужна фабрика (* класс (class) используемый для создания экземпляров (instance) других классов. Фабрика нужна, чтобы изолировать создание объектов конкретного класса) для представлений? Что ж, для работы с Backbone.js имеются некоторые распространенные шаблоны. Один из них относится к созданию и использованию представлений.
1 |
var ViewClass = Backbone.View.extend({ /* logic here */ }); |
2 |
var view = new ViewClass(); |
Хорошо, если вы инициализировали представления только единожды и оставили их в таком состоянии. Сразу после изменения данных мы обычно вызываем методы представления и обновляем контент его объекта el
. Другим очень распространенным подходом является повторное создание всего представления или замена всего элемента DOM. Однако это не совсем удачный выбор с точки зрения производительности. Поэтому мы обычно используем вспомогательный класс, при помощи которого создается и возвращается при необходимости один экземпляр представления.
Определение компонентов
У нас имеется пространство имен, так что теперь мы можем приступить к созданию компонентов. Ниже показано, как выглядит код для главного меню:
1 |
// views/menu.js
|
2 |
app.views.menu = Backbone.View.extend({ |
3 |
initialize: function() {}, |
4 |
render: function() {} |
5 |
});
|
Мы создали свойство под названием menu
, в котором хранится класс для навигационного меню. Позже мы можем добавить в модуле фабрики метод, при помощи которого создается его экземпляр.
1 |
var ViewsFactory = { |
2 |
menu: function() { |
3 |
if(!this.menuView) { |
4 |
this.menuView = new api.views.menu({ |
5 |
el: $("#menu") |
6 |
});
|
7 |
}
|
8 |
return this.menuView; |
9 |
}
|
10 |
};
|
Выше показано, как мы создадим все представления, и благодаря этому подходу будет гарантировано, что мы получаем только одно представление того же самого типа. В большинстве случаев этот подход работает замечательно.
Поток данных
В качестве точки входа приложения выступает App.js
и содержащийся в его коде метод init
. Ниже показано, что мы вызовем в обработчике события onload
для объекта window
:
1 |
window.onload = function() { |
2 |
app.init(); |
3 |
}
|
После этого управление передается указанному маршрутизатору. При помощи него на основании URL-адреса выбирается, какой обработчик запускать. В Backbone.js нет типичной архитектуры Модель-Представление-Контроллер (* MVC – Model-View-Controller). Контроллер отсутствует, и большая часть логики помещается в представления. Так что мы подвязываем модели непосредственно к методам представлений, и пользовательский интерфейс после изменения данных немедленно обновляется.
Управление данными
Наиболее важная часть нашего небольшого проекта – данные. Нам необходимо управлять нашими заданиями, так что давайте начнем с них. Ниже приводится определение нашей модели.
1 |
// models/ToDo.js
|
2 |
app.models.ToDo = Backbone.Model.extend({ |
3 |
defaults: { |
4 |
title: "ToDo", |
5 |
archived: false, |
6 |
done: false |
7 |
}
|
8 |
});
|
Всего лишь три поля. В первое помещается текст задания, а при помощи остальных двух, которые являются флажками, отмечается статус записи.
Все объекты в рассматриваемом здесь фреймворке являются собственно распределителями событий. И поскольку модель изменяется при помощи сеттеров, фреймворк знает, когда обновляются данные, и может оповестить остальные части системы об этом. Как только вы подключили что-то для этих оповещений, ваше приложение отреагирует на изменения в модели. Это действительно мощная возможность Backbone.js.
Как я упомянул в начале, у нас будет множество записей и мы организуем их в виде коллекции под названием ToDos
.
1 |
// collections/ToDos.js
|
2 |
app.collections.ToDos = Backbone.Collection.extend({ |
3 |
initialize: function(){ |
4 |
this.add({ title: "Learn JavaScript basics" }); |
5 |
this.add({ title: "Go to backbonejs.org" }); |
6 |
this.add({ title: "Develop a Backbone application" }); |
7 |
},
|
8 |
model: app.models.ToDo |
9 |
up: function(index) { |
10 |
if(index > 0) { |
11 |
var tmp = this.models[index-1]; |
12 |
this.models[index-1] = this.models[index]; |
13 |
this.models[index] = tmp; |
14 |
this.trigger("change"); |
15 |
}
|
16 |
},
|
17 |
down: function(index) { |
18 |
if(index < this.models.length-1) { |
19 |
var tmp = this.models[index+1]; |
20 |
this.models[index+1] = this.models[index]; |
21 |
this.models[index] = tmp; |
22 |
this.trigger("change"); |
23 |
}
|
24 |
},
|
25 |
archive: function(archived, index) { |
26 |
this.models[index].set("archived", archived); |
27 |
},
|
28 |
changeStatus: function(done, index) { |
29 |
this.models[index].set("done", done); |
30 |
}
|
31 |
});
|
Код коллекции начинается с метода initialize
. В нашем случае мы добавили несколько заданий по умолчанию. Конечно же, в реальном мире разработки информация будет поступать из базы данных или еще откуда-то. Но чтобы не рассеивать ваше внимание, мы сделаем это вручную. Другой характерный для коллекций момент – задание значения свойства model
. При помощи него классу сообщается информация о типе хранимых данных. За счет остальных методов реализуется логика, касающаяся возможностей нашего приложения. При помощи функций up
и down
меняется порядок расположения записей. Для простоты мы будем идентифицировать каждое задание лишь при помощи индекса массива коллекции. Это означает, что если нам нужно получить одну конкретную запись, то мы должны указать ее индекс. Таким образом, для изменения порядка расположения записей нам необходимо лишь поменять местами элементы в массиве. Как вы можете догадаться из кода выше, this.models
– тот массив, о котором мы говорим. При помощи archive
и changeStatus
задаются значения свойств переданного элемента. Мы добавляем эти методы здесь, так как у представлений будет доступ к коллекции ToDos
, а не непосредственно к заданиям.
Также нам не нужно создавать какие-либо модели класса app.models.ToDo
, однако нам действительно необходимо создать образец коллекции app.collections.ToDos
.
1 |
// App.js
|
2 |
init: function() { |
3 |
this.content = $("#content"); |
4 |
this.todos = new api.collections.ToDos(); |
5 |
return this; |
6 |
}
|
Отображение нашего первого представления (Главного навигационного меню)
Первое, что нам нужно отобразить, – главное навигационное меню приложения.
1 |
// views/menu.js
|
2 |
app.views.menu = Backbone.View.extend({ |
3 |
template: _.template($("#tpl-menu").html()), |
4 |
initialize: function() { |
5 |
this.render(); |
6 |
},
|
7 |
render: function(){ |
8 |
this.$el.html(this.template({})); |
9 |
}
|
10 |
});
|
В файле выше только девять строк кода, однако там происходит много крутых вещей. Во-первых, определение шаблона. Помните, что мы добавили Underscore.js в приложение? Мы будем использовать ее шаблонизатор (* программное обеспечение для комбинирования шаблонов с моделью данных для получения конечных документов), поскольку он хорошо работает и им легко пользоваться.
1 |
_.template(templateString, [data], [settings]) |
В результате у вас получается функция, принимающая объект, содержащий вашу информацию в виде пар ключ-значение, и templateString
– это HTML-разметка. Хорошо, значит в вышеприведенную функцию передается строка с HTML-разметкой, но зачем там используется $("#tpl-menu").html()
? При разработке небольших одностраничных приложений мы обычно добавляем шаблоны непосредственно в документ следующим образом:
1 |
// index.html |
2 |
<script type="text/template" id="tpl-menu"> |
3 |
<ul> |
4 |
<li><a href="#">List</a></li> |
5 |
<li><a href="#archive">Archive</a></li> |
6 |
<li class="right"><a href="#new">+</a></li> |
7 |
</ul> |
8 |
</script>
|
И поскольку это тег script, то шаблон не показывается пользователю. С другой стороны, это обычный узел DOM, так что мы могли бы получить его контент при помощи jQuery. Таким образом, при помощи небольшого фрагмента кода выше просто получается контент того тега script.
Метод render
очень важен в Backbone.js. Это функция, при помощи которой отображаются данные. Обычно вы привязываете генерируемые моделью события непосредственно к этому методу. Однако в случае с главным меню нам этого не нужно.
1 |
this.$el.html(this.template({})); |
this.$el
– объект, созданный фреймворком, и у каждого представления он имеется по умолчанию (перед el
имеется $
, поскольку мы подключили jQuery). И по умолчанию его значением является пустой <div></div>
. Естественно, вы можете изменить его при помощи свойства tagName
. Однако здесь важнее то, что мы не присваиваем значение тому объекту непосредственно. Мы его не меняем, мы изменяем только его контент. Между строкой выше и следующей строкой существует большая разница:
1 |
this.$el = $(this.template({})); |
Смысл в том, что если вы хотите увидеть изменения в браузере, то вы должны для начала вызвать метод render, чтобы добавить представление в DOM. Иначе будет добавлен только пустой элемент div. Также может быть другой сценарий, когда у вас имеются вложенные представления. А поскольку вы меняете непосредственно свойство, то родительский компонент не обновляется. Также может нарушиться привязка к событиям, и вам нужно будет снова подключать обработчики. Таким образом, вам действительно необходимо изменять только контент this.$el
, а не значение свойства.
Теперь представление готово, и нам необходимо его инициализировать. Давайте добавим его в наш модуль фабрики:
1 |
// App.js
|
2 |
var ViewsFactory = { |
3 |
menu: function() { |
4 |
if(!this.menuView) { |
5 |
this.menuView = new api.views.menu({ |
6 |
el: $("#menu") |
7 |
});
|
8 |
}
|
9 |
return this.menuView; |
10 |
}
|
11 |
};
|
Наконец, просто вызовите метод menu
в области для самонастройки приложения:
1 |
// App.js
|
2 |
init: function() { |
3 |
this.content = $("#content"); |
4 |
this.todos = new api.collections.ToDos(); |
5 |
ViewsFactory.menu(); |
6 |
return this; |
7 |
}
|
Обратите внимание на то, что хотя мы и создаем новый образец класса навигационного меню, мы передаем уже существующий элемент DOM $("#menu")
. Поэтому в свойстве this.$el
внутри представления содержится ссылка собственно на $("#menu")
.
Добавление маршрутов
В Backbone.js имеется поддержка операций добавления состояний в стек. Другими словами, вы можете манипулировать текущим URL-адресом браузера и переходить между страницами. Однако мы воспользуемся старыми добрыми URL-адресами с хеш-префиксом (* хеш url – все, что идет после символа #), например /#edit/3
.
1 |
// App.js
|
2 |
var Router = Backbone.Router.extend({ |
3 |
routes: { |
4 |
"archive": "archive", |
5 |
"new": "newToDo", |
6 |
"edit/:index": "editToDo", |
7 |
"delete/:index": "delteToDo", |
8 |
"": "list" |
9 |
},
|
10 |
list: function(archive) {}, |
11 |
archive: function() {}, |
12 |
newToDo: function() {}, |
13 |
editToDo: function(index) {}, |
14 |
delteToDo: function(index) {} |
15 |
});
|
Выше приведен наш маршрутизатор. В хеше (* коллекция пар ключ-значение, причем ключом является строка) имеется пять маршрутов. Ключ – то, что вы будете набирать в адресной строке браузера, а значение – функция, которая будет вызываться. Обратите внимание на то, что в составе двух маршрутов имеется :index
. Это соответствует синтаксису, которого вы должны придерживаться, если хотите реализовать поддержку динамических URL-адресов. В нашем случае, если вы наберете #edit/3
, то при вызове editToDo
в качестве значения параметра index
будет передано 3. В последней паре содержится пустая строка, и это означает, что она используется для реализации перехода на домашнюю страницу нашего приложения.
Отображение всего списка заданий
На данный момент мы создали главное представление для нашего проекта. Данные будут переданы в него из коллекции и выведены на экран. Мы могли бы использовать одно и то же представление для двух целей – отображение всех активных заданий и тех, что помещены в архив.
Перед тем как продолжить реализацию представления для списка, давайте посмотрим, как оно собственно инициализируется.
1 |
// in App.js views factory
|
2 |
list: function() { |
3 |
if(!this.listView) { |
4 |
this.listView = new api.views.list({ |
5 |
model: api.todos |
6 |
});
|
7 |
}
|
8 |
return this.listView; |
9 |
}
|
Обратите внимание на то, что мы передаем коллекцию. Это важно, поскольку позже мы воспользуемся this.model
для получения сохраненных данных. Фабрика возвращает наше представление для списка, однако именно при помощи маршрутизатора оно должно быть добавлено на страницу.
1 |
// in App.js's router
|
2 |
list: function(archive) { |
3 |
var view = ViewsFactory.list(); |
4 |
api
|
5 |
.title(archive ? "Archive:" : "Your ToDos:") |
6 |
.changeContent(view.$el); |
7 |
view.setMode(archive ? "archive" : null).render(); |
8 |
}
|
Пока что метод list
в маршрутизаторе вызывается без каких-либо аргументов. Так что представление находится не в режиме archive
; при помощи него будут отображаться только текущие задания.
1 |
// views/list.js
|
2 |
app.views.list = Backbone.View.extend({ |
3 |
mode: null, |
4 |
events: {}, |
5 |
initialize: function() { |
6 |
var handler = _.bind(this.render, this); |
7 |
this.model.bind('change', handler); |
8 |
this.model.bind('add', handler); |
9 |
this.model.bind('remove', handler); |
10 |
},
|
11 |
render: function() {}, |
12 |
priorityUp: function(e) {}, |
13 |
priorityDown: function(e) {}, |
14 |
archive: function(e) {}, |
15 |
changeStatus: function(e) {}, |
16 |
setMode: function(mode) { |
17 |
this.mode = mode; |
18 |
return this; |
19 |
}
|
20 |
});
|
Свойство mode
будет использовано при рендеринге. Если его значением является "archive"
, то будут отображены только находящиеся в архиве записи. events
– объект, который мы заполним очень скоро. Это место, где мы назначаем событиям DOM соответствующие обработчики. Остальные методы используются для отклика на действия со стороны пользователя и непосредственно для реализации необходимых возможностей приложения. Например, при помощи priorityUp
и priorityDown
меняется порядок расположения заданий. За счет archive
элемент перемещается в архив. При помощи changeStatus
отмечается, что задание выполнено.
В методе initialize
происходит нечто интересное. Ранее мы упомянули, что обычно вы будете подвязывать к событиям, возникающим при изменениях в модели (коллекции в нашем случае), метод render
представления. Вы можете написать this.model.bind('change', this.render)
. Однако очень скоро вы заметите, что ключевое слово this
в методе render
не будет указывать на само представление. Так происходит из-за смены области видимости. В качестве обходного пути мы создаем обработчик с уже заданной областью видимости. Именно для этого и служит функция bind
Underscore.
А ниже приводится реализация метода render
.
1 |
// views/list.js
|
2 |
render: function() {) |
3 |
var html = '<ul class="list">', |
4 |
self = this; |
5 |
this.model.each(function(todo, index) { |
6 |
if(self.mode === "archive" ? todo.get("archived") === true : todo.get("archived") === false) { |
7 |
var template = _.template($("#tpl-list-item").html()); |
8 |
html += template({ |
9 |
title: todo.get("title"), |
10 |
index: index, |
11 |
archiveLink: self.mode === "archive" ? "unarchive" : "archive", |
12 |
done: todo.get("done") ? "yes" : "no", |
13 |
doneChecked: todo.get("done") ? 'checked=="checked"' : "" |
14 |
});
|
15 |
}
|
16 |
});
|
17 |
html += '</ul>'; |
18 |
this.$el.html(html); |
19 |
this.delegateEvents(); |
20 |
return this; |
21 |
}
|
Мы перебираем все модели коллекции и генерируем HTML-строку, которая позднее вставляется в DOM-элемент представления. Выполняется несколько проверок для определения того, принадлежат ли задания к тем, что находятся в архиве, или к текущим. При помощи флажка отмечается, что задание выполнено. Так что, для того чтобы это указать, нам необходимо передать атрибут checked=="checked"
тому элементу. Вы, вероятно, заметили, что мы используем this.delegateEvents()
. В нашем случае это необходимо, поскольку мы отвязываем представление от DOM и привязываем его к нему. Да, мы не заменяем главный элемент, однако обработчики событий удаляются. Поэтому нам необходимо указать Backbone.js, что их следует снова подключить. Использованный в коде выше шаблон выглядит следующим образом:
1 |
// index.html |
2 |
<script type="text/template" id="tpl-list-item"> |
3 |
<li class="cf done-<%= done %>" data-index="<%= index %>"> |
4 |
<h2> |
5 |
<input type="checkbox" data-status <%= doneChecked %> /> |
6 |
<a href="javascript:void(0);" data-up>↑</a> |
7 |
<a href="javascript:void(0);" data-down>↓</a> |
8 |
<%= title %> |
9 |
</h2> |
10 |
<div class="options"> |
11 |
<a href="#edit/<%= index %>">edit</a> |
12 |
<a href="javascript:void(0);" data-archive><%= archiveLink %></a> |
13 |
<a href="#delete/<%= index %>">delete</a> |
14 |
</div> |
15 |
</li> |
16 |
</script>
|
Обратите внимание, что указан класс CSS под названием done-yes
, при помощи которого задание отображается с зеленым фоном. Помимо этого имеется множество ссылок, которые мы будем использовать для реализации необходимых функциональных возможностей. У них всех имеются собственные атрибуты HTML5 Data. У главного узла элемента, li
, имеется data-index
. При помощи значения этого атрибута указывается индекс задания в коллекции. Обратите внимание на то, что специальные выражения, обернутые в <%= ... %>
, передаются в функцию template
. Это данные, передаваемые в шаблон.
Пора добавить некоторые события для представления.
1 |
// views/list.js
|
2 |
events: { |
3 |
'click a[data-up]': 'priorityUp', |
4 |
'click a[data-down]': 'priorityDown', |
5 |
'click a[data-archive]': 'archive', |
6 |
'click input[data-status]': 'changeStatus' |
7 |
}
|
В Backbone.js для определения событий используется просто хеш. Сначала вы указываете имя события, а затем – селектор. Значения свойств являются собственно методами представления.
1 |
// views/list.js
|
2 |
priorityUp: function(e) { |
3 |
var index = parseInt(e.target.parentNode.parentNode.getAttribute("data-index")); |
4 |
this.model.up(index); |
5 |
},
|
6 |
priorityDown: function(e) { |
7 |
var index = parseInt(e.target.parentNode.parentNode.getAttribute("data-index")); |
8 |
this.model.down(index); |
9 |
},
|
10 |
archive: function(e) { |
11 |
var index = parseInt(e.target.parentNode.parentNode.getAttribute("data-index")); |
12 |
this.model.archive(this.mode !== "archive", index); |
13 |
},
|
14 |
changeStatus: function(e) { |
15 |
var index = parseInt(e.target.parentNode.parentNode.getAttribute("data-index")); |
16 |
this.model.changeStatus(e.target.checked, index); |
17 |
}
|
Здесь мы используем e.target
, передаваемый в обработчик. В нем находится ссылка на элемент DOM, сгенерировавший событие. Мы получаем индекс выбранного задания и обновляем модель в коллекции. После добавления этих четырех функций наш класс готов, и теперь данные отображаются на странице.
Как было упомянуто ранее, мы будем использовать то же самое представление и для страницы Archive
.
1 |
list: function(archive) { |
2 |
var view = ViewsFactory.list(); |
3 |
api
|
4 |
.title(archive ? "Archive:" : "Your ToDos:") |
5 |
.changeContent(view.$el); |
6 |
view.setMode(archive ? "archive" : null).render(); |
7 |
},
|
8 |
archive: function() { |
9 |
this.list(true); |
10 |
}
|
Выше представлен тот же обработчик для маршрута, что и ранее, однако в этот раз ему в качестве аргумента передается значение true
.
Реализация возможности добавления и редактирования заданий
Следуя примеру представления для списка, мы могли бы создать другое, при помощи которого отображается форма для добавления и редактирования заданий. Ниже показано, как создается класс для этого:
1 |
// App.js / views factory
|
2 |
form: function() { |
3 |
if(!this.formView) { |
4 |
this.formView = new api.views.form({ |
5 |
model: api.todos |
6 |
}).on("saved", function() { |
7 |
api.router.navigate("", {trigger: true}); |
8 |
})
|
9 |
}
|
10 |
return this.formView; |
11 |
}
|
Код очень похож на тот, что видели ранее. Однако, в этот раз нам необходимо кое-что выполнить после отправления формы, а именно перенаправить пользователя на главную страницу. Как я сказал ранее, каждый объект, который наследует характеристики классов Backbone.js, является собственно распределителем событий. Имеются методы вроде on
и trigger
, которыми вы можете воспользоваться.
Перед тем как мы продолжим разбираться с представлением, давайте взглянем на HTML-шаблон:
1 |
<script type="text/template" id="tpl-form"> |
2 |
<form> |
3 |
<textarea><%= title %></textarea> |
4 |
<button>save</button> |
5 |
</form> |
6 |
</script>
|
У нас имеется textarea
и button
. В шаблон должен передаваться аргумент title
, значением которого должна быть пустая строка, если мы добавляем новое задание.
1 |
// views/form.js
|
2 |
app.views.form = Backbone.View.extend({ |
3 |
index: false, |
4 |
events: { |
5 |
'click button': 'save' |
6 |
},
|
7 |
initialize: function() { |
8 |
this.render(); |
9 |
},
|
10 |
render: function(index) { |
11 |
var template, html = $("#tpl-form").html(); |
12 |
if(typeof index == 'undefined') { |
13 |
this.index = false; |
14 |
template = _.template(html, { title: ""}); |
15 |
} else { |
16 |
this.index = parseInt(index); |
17 |
this.todoForEditing = this.model.at(this.index); |
18 |
template = _.template($("#tpl-form").html(), { |
19 |
title: this.todoForEditing.get("title") |
20 |
});
|
21 |
}
|
22 |
this.$el.html(template); |
23 |
this.$el.find("textarea").focus(); |
24 |
this.delegateEvents(); |
25 |
return this; |
26 |
},
|
27 |
save: function(e) { |
28 |
e.preventDefault(); |
29 |
var title = this.$el.find("textarea").val(); |
30 |
if(title == "") { |
31 |
alert("Empty textarea!"); return; |
32 |
}
|
33 |
if(this.index !== false) { |
34 |
this.todoForEditing.set("title", title); |
35 |
} else { |
36 |
this.model.add({ title: title }); |
37 |
}
|
38 |
this.trigger("saved"); |
39 |
}
|
40 |
});
|
Код представления содержит всего лишь 40 строк, однако он выполняет свою задачу. Имеется всего один обработчик события, которое возникает при нажатии кнопки save. Метод render ведет себя по-разному в зависимости от значения переданного аргумента index
. Например, если мы редактируем запись, то передаем индекс и получаем конкретную модель. Если нет, то форма пуста и будет создано новое задание. В коде выше есть несколько интересный моментов. Во-первых, в render ми воспользовались методом .focus()
для перемещения фокуса в форму сразу после отображения представления. Опять-таки, должна быть вызвана функция delegateEvents
, поскольку форма могла бы быть отвязана и привязана снова. Метод save
начинается с e.preventDefault()
. В результате поведение кнопки по умолчанию отменяется, которое в некоторых случаях может заключаться в отправлении формы. В конце, после того как все выполнено, мы генерируем событие saved
, при помощи которого остальные части системы оповещаются о том, что задание сохранилось в коллекцию.
Нам необходимо реализовать два метода для маршрутизатора.
1 |
// App.js
|
2 |
newToDo: function() { |
3 |
var view = ViewsFactory.form(); |
4 |
api.title("Create new ToDo:").changeContent(view.$el); |
5 |
view.render() |
6 |
},
|
7 |
editToDo: function(index) { |
8 |
var view = ViewsFactory.form(); |
9 |
api.title("Edit:").changeContent(view.$el); |
10 |
view.render(index); |
11 |
}
|
Разница между ними заключается в том, что мы передаем index, если переход выполняется по маршруту edit/:index
. И, конечно же, заголовок страницы меняется соответствующим образом.
Реализация возможности удаления записи из коллекции
Для реализации этой возможности нам не нужно представление. Все необходимое может быть выполнено непосредственно в обработчике маршрутизатора.
1 |
delteToDo: function(index) { |
2 |
api.todos.remove(api.todos.at(parseInt(index))); |
3 |
api.router.navigate("", {trigger: true}); |
4 |
}
|
Нам известен индекс задания, которое мы хотим удалить. Имеется метод remove
класса коллекции, который принимает объект модели. В конце перенаправляем пользователя на главную страницу, где отображается обновленный список.
Заключение
Backbone.js предоставляет вам все возможности, необходимые для создания полнофункциональных одностраничных приложений. Мы бы могли даже привязать его к RESTful веб-службе (* веб-служба, построенная с учётом REST (Representational State Transfer – передача состояния представления) на стороне сервера, и при помощи этого фреймворка данные вашего приложения были бы синхронизированы с базой данных. За счет событийно-управляемого подхода разработки (* стиль проектирования программных систем, при котором поведение компонента системы определяется набором возможных внешних событий и ответных реакций компонента на них) нам становится легче использовать метод модульного программирования (* один из ранних методов проектирования программ. Вся программа разбивается на модули, каждый из которых выполняет одну функцию и содержит в себе весь необходимый для этого код и переменные, что позволяет программировать и отлаживать его отдельно. Затем модули постепенно собираются вместе, пока не будет реализована вся система. Этот подход позволил уменьшить сложность разработки и отладки больших программ. Принципы модульного программирования стали составной частью ООП) и построить хорошую архитектуру. Лично я использую Backbone.js для нескольких проектов, и он отлично работает.