Створення односторінкового додатка для складання списку завдань за допомогою 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 нема дуже строгих архітектурних принципів, яких нам варто дотримуватися. Це є однією з переваг даного фреймворка. Так що, перед тим як взятися за реалізацію бізнес-логіки, давайте розглянемо основні принципи.
Визначення простору імен
(* набір правил іменування, що регулює видимість об'єктів у програмі або хост-комп'ютерів у комп'ютерній мережі. Простір імен може бути плоским (flat namespace) та ієрархічним (hierarchical namespace). Передбачено, що всі імена у просторі імен мають бути унікальними). Згідно з усталеною практикою ваш код потрібно розташовувати в його власній області видимості (* області тексту програми, де можна використати заданий ідентифікатор (ім'я змінної, іменована константи, назва функції тощо). Область видимості можна змінити, перевизначивши ідентифікатор, але краще просто не використовувати в різних блоках програми однакові імена). Реєстрація глобальних змінних або функцій – погана ідея. Ми створимо одну модель, одну колекцію, маршрутизатор та декілька представлень 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 для декількох проектів, і він відмінно працює.