- PVSM.RU - https://www.pvsm.ru -
Этот пост продолжение серии, начатой здесь [1]. Сегодня мы создадим, так называемый, «список дел» (Todo-
list из проекта TodoMVC [2]). За основу возьмем варинт, сделанный на Angular [3], и попробуем воссоздать функционал на derby [4].
Итак, посмотрим, что у нас есть в ангуляровском варианте, и как оно работает (поиграйтесь сами — все проверьте):
enter
Исходя из наших целей (узнать лучше derbyjs), мы не будем здесь придумывать стили, они уже написаны и используются без изменений в большинстве реализаций TodoMVC. Просто возьмем css-файл [5]. Беглый взгляд по нему показывает, что нам нужно будет взять еще и картинку для фона bg.png [6]. Так же, в качетстве каркаса возьмем сгенерированный ангуляром html (я скопировал его с помощью инструментов разработчика в браузере и немного почистил от ангуляровских деректив).
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<form id="todo-form">
<input id="new-todo" placeholder="What needs to be done?" autofocus">
</form>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>hello</label>
<button class="destroy"> </button>
</div>
<form >
<input class="edit">
</form>
</li>
</ul>
</section>
<footer id="footer">
<span id="todo-count"><strong>0</strong>
<span>items left</span>
</span>
<ul id="filters">
<li><a href="/" class="selected">All</a></li>
<li><a href="/active">Active</a></li>
<li><a href="/completed">Completed</a></li>
</ul>
<button id="clear-completed">Clear completed (0)</button>
</footer>
</section>
Как видно, наш html состоит из 3-х основных блоков:
Так, давайте рассуждать. Что у нас будет в проекте? Будет файл стилей, будет отдача статичесих данных (какртинки фона), будут html-шаблоны, так же будет как минимум 2-файла — две части дерби приложения (серверная часть, и само дерби-приложение). Исходя из этого всего, я накидал такую файловую структуру приложеня (можете сделать любую другую):
public/
bg.png
app # Дерби-приложение
views/
index.html
css/
index.css
index.js # Код дерби-приложения
server.js # Серверная часть дерби
package.json
Обратите внимании, css-файл лежит внутри папки app, а не внутри public. Это связано с тем, что дерби работает с стилями по особому. В итоге они будут будут вставлены напрямую в head страницы в теги style — как показали исследования google-а (со слов создателей derby) — это лучший по скорости способ размещения стилей.
Итак, как я уже говорил в прошлом уроке, все что находится в папке app — изоморфное [7] приложение-дерби. Слова «изоморфное» мне не нравится и я его буду опускать, буду говорить просто дерби-приложение в противоположность к «серверной части дерби». Смысл здесь в том, что все эти файлы вместе (все что в app), единым бандлом (куском) будут отдаваться клиентскому браузеру, поэтому я их вместе и положил.
Вообще (на будущее), можно разбивать проект на несколько дерби-приложений, например, клиентская часть и админка. Это оправдано по двум причинам, чтобы не отдавать лишние данные (шаблоны, стили, код), и чтобы уменьшить связанность. То есть будет так: в проекте будет одна серверная часть и несколько дерби-приложений (в данном случае два).
В файле package.json в качестве зависимостей будут все те же два модуля: derby@0.6.0-alpha5 и derby-starter.
Создем файловую структуру. Фоновую картику и стили качаем по ссылкам, которые я указал вначале, package.json создаем при помощи npm init
(можете посмотреть в предыдущем уроке).
Html немножко подправим, во-первых, как и в предыдущем примере, он должен находиться в предопределенном шаблоне Body:
, во-вторых вынесем header, main и footer в отдельные derby-шаблоны.
<Body:>
<section id="todoapp">
<view name="header"/>
<view name="main"/>
<view name="footer"/>
</section>
<header:>
<header id="header">
<h1>todos</h1>
<form id="todo-form">
<input id="new-todo" placeholder="What needs to be done?" autofocus">
</form>
</header>
<main:>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>hello</label>
<button class="destroy"> </button>
</div>
<form >
<input class="edit">
</form>
</li>
</ul>
</section>
<footer:>
<footer id="footer">
<span id="todo-count"><strong>0</strong>
<span>items left</span>
</span>
<ul id="filters">
<li><a href="/" class="selected">All</a></li>
<li><a href="/active">Active</a></li>
<li><a href="/completed">Completed</a></li>
</ul>
<button id="clear-completed">Clear completed (0)</button>
</footer>
Как вы могли заметить, вызов собственных шаблонов происходит при помощи тега view
, где в атрибуте name
, задается имя шаблона.
Для начала, создадим минимальный работающий код, чтобы иметь возможность видеть в браузере результат и наращивать функционал.
Файл server.js из прошлого примера немного расширен, чтобы учесть структуру проекта и отдавать статические файлы.
var server = require('derby-starter');
var appPath = __dirname + '/app';
var options = {
static: __dirname + '/public'
};
server.run(appPath, options);
Напомню, что из-за учебной природы проекта, в качестве серверной части, мы используем модуль derby-starter. Если заглянуть внутрь, то отдача статических файлов там — это классическое испльзование express-овского static-middlware. Посмотрите сами [8].
var derby = require('derby');
var app = module.exports = derby.createApp('todos', __filename);
// Делаем app глобальной, чтобы иметь к ней доступ в консоле браузера
// (конечно только на время разработки)
global.app = app;
app.loadViews (__dirname+'/views');
app.loadStyles(__dirname+'/css');
app.get('/', getTodos);
function getTodos(page, model){
page.render();
}
Все, запускаем npm start (или напрямую node server.js), видим в браузере http://localhost:3000/ [9] результат:
Стили с версткой подцепились. Начало положено.
В прошлом уроке я говорил, что дерби-разработчик должен начинать разработку с разбиения проекта на url-адреса. Это связано с возмжностью дерби генерировать страницы как на клиенте так и на сервере, что очень любят поисковые системы. Итак, изучая ангуляровский вариант мы заметили, что в футере есть 3 ссылки, меняющие url и соответственно фильтр по задачам. Здесь мы понимаем, что у нас в приложении должно быть 3 обработчика get-запросов. Что-то типа:
app.get('/', getAllTodos);
app.get('/active', getActiveTodos);
app.get('/completed', getCompletedTodos);
Это было бы оправданно, если бы все эти страницы были разными, но у нас, единственное отличение между ними — фильтр, поэтому постараемся по минимуму дублировать код.
Сами задачи у нас будут храниться в коллекции todos
. Каждая задача будет представленна двумя полями:
К этому нужно добавить, что у каждой задачи еще, конечно же, будет поле id — derby добавит его автоматически при добавлении элемента в коллекцию.
Итак, в соответствии с методологией дерби, в контроллере (функции, обрабатывающей запрос к url) до вызова render мы должны подготовить данные и зарегистрировать подписки на обновление данных. Получается обработчик, схематично должен быть примерно таким:
function getTodos(page, model){
model.subscribe('todos', function(){
page.render();
});
}
Так примерно и будет, но прежде, чем двигаться дальше (к тому чтобы сделать один контроллер для всех трех запросов, только чтобы фильтры по задачам были разные) нужно узнать несколько вещей о моделях дерби:
В прошлом уроке я говорил о так-называемых «путях». Мы исопльзуем их в операциях с моделями. Например при подписке на данные: model.subscribe('путь'), при получении и при записи данных в модель: model.get('путь')
, model.set('путь', значение)
. Примеры путей:
Так вот. Первый сергмент пути — это, как вы поняли, имя коллекции. Это имя в дерби может начинаться либо с латинской буквы, либо с сиволов $ или _. Все коллекции, начинающиеся с $ и _ особенные, они не синхронизируются с сервером (являются локальными для модели, а модель в приложении-дерби создается всего одна). Коллекции начинающиеся с $ зарезервированы дерби для собственных нужд. Коллекции же, начинающиеся с символа подчеркивания используются разработчиками.
Давайте, проведем небольшой эксперимент. Откройте в браузере консоль разработчика и наберите app.model.get()
— поизучайте вывод.
Среди "_"-коллекций есть одна особенная — _page
, она затирается каждый раз при смене url — это делает ее очень удобной для хранения всевозможных рабочих данных. В этом уроке вы еще увидите примеры.
Перейдем к фильтрам. Если вы читали документацию по моделям, вы знаете, что в дерби, есть различные механизмы, позволяющие облегчить работу с реактивными данными. Это, например, реактивные функции, подписки на различные события, происходящие с данными, фильтры-данных, сортировщики даннх.
Обсудим фильтры. Как нам реализовать, например фильтр, показывающий только активные задачи:
Регистрируем по определенному имени функцию-фильтр (имя обязательно для сериализации в бандл). Документация говорит, что регистрировать их нужно строго в app.on('model')
app.on('model', function(model) {
model.fn('completed', function(item) {
return item.completed;
});
});
И далее в контроллере, используем этот фильтр для фильтрации коллекции todos:
function getPage(page, model){
model.subscribe('todos', function() {
var filter = model.filter('todos', 'completed')
filter.ref('_page.todos');
page.render();
});
}
Очень важна здесь строка filter.ref('_page.todos');
, в ней отфильтрованный «todos» становится доступным по пути _page.todos
. Собрав все вместе, я предлагаю вот такой код фильтров с контроллерами:
app.on('model', function(model) {
model.fn('all', function(item) { return true; });
model.fn('completed', function(item) { return item.completed;});
model.fn('active', function(item) { return !item.completed;});
});
app.get('/', getPage('all'));
app.get('/active', getPage('active'));
app.get('/completed', getPage('completed'));
function getPage(filter){
return function(, page, model){
model.subscribe('todos', function() {
model.filter('todos', filter).ref('_page.todos');
page.render();
});
}
}
Как вы, наверное, заметили, чтобы все унифицировать, пришлось сделать фальш-фильтр «all», но думаю это не большая плата за отсутствие дублей.
Ладно мы немножно отвлеклись. Давайте оживим приложение.
Инпут для ввода данных в верстке у нас выглядет так:
<form id="todo-form">
<input id="new-todo" placeholder="What needs to be done?" autofocus">
</form>
Классический паттерн в дерби (как и во многих современных фреймворках) — рективное связывание. Свяжем значение, вводимое в input, с каким-нибудь путем в _page
. Так же зарегистрируем обработчик события submit
формы, для того, чтобы обрабатывать нажание на enter
:
<form id="todo-form" on-submit="addTodo(_page.newTodo)">
<input id="new-todo" placeholder="What needs to be done?" autofocus value="{{_page.newTodo}}">
</form>
Вместо, on-submit
мы естественно могли бы написать on-click
, on-keyup
, on-focus
— то есть это стандартный способ обработки событий в дерби. Обротчик помещаем в app.proto
(когда будем обсуждать дерби-компоненты, увидем, что каждая компонента хранит свои обработчики в себе, но пока делаем так):
app.proto.addTodo = function(newTodo){
if (!newTodo) return;
this.model.add('todos', {
text: newTodo,
completed: false
});
this.model.set('_page.newTodo', '');
};
Проверяем не пустой ли текст, добавляем задачу в коллекцию, очищаем input
. Возможно вы заметили, что в обработчике у нас только один параметр, если бы нам, для каких-то нужд, понадобились ссылки на объект-событие или на сам html-элемент, нам нужно было бы явно прописать это в html таким образом: on-submit="addTodo(_page.newTodo, $event, $element)"
, $event
и $element
— особые параметры, заполняются самим дерби.
Теперь вывод отфильтрованного списка задач — отредактируем наш ul
-элемент:
<ul id="todo-list">
{{each _page.todos as #todo, #index}}
<li class="{{if #todo.completed}}completed{{/}}">
<div class="view">
<input class="toggle" type="checkbox" checked="{{#todo.completed}}">
<label>{{#todo.text}}</label>
<button class="destroy"> </button>
</div>
<form>
<input class="edit">
</form>
</li>
{{/each}}
</ul>
Так, что сделали:
li
lable
checkbox
к todo.completed
li
устанавливаем класс completed
, если задача выполнена.Делается элементарно:
<button class="destroy" on-click="delTodo(#todo.id)"> </button>
app.proto.delTodo = function(todoId){
this.model.del('todos.' + todoId);
};
Причем можно было еще короче:
<button class="destroy" on-click="model.del('todos.' + #todo.id)"> </button>
Удаление всех «завершенных» задач аналогично (кнопка «Clear completed» снизу справа):
<button id="clear-completed" on-click="clearCompleted()">
Clear completed (0)
</button>
app.proto.clearCompleted = function(){
var todos = this.model.get('todos');
for (var id in todos) {
if (todos[id].completed) this.model.del('todos.'+id);
}
}
По двойному щелчку мыши, задача должна перейти в режим редактирования. Судя по верстке, при переходе в этот режим, нам нужно будет добавить класс editing
соответствующему элементу li
. Так же попутно, нужно будет избавиться от выделения, которое возникает при двойном нажатии и правильно поставить фокус на нужный нам input
.
Предлагаю сделать следующим образом: информацию о редактируемой задаче будем хранить, используя путь — _page.edit. Там будем хранить id редактируемой задачи, и текст.
enter
.
Итак, реализуем все это:
<ul id="todo-list">
{{each _page.todos as #todo}}
<li class="{{if #todo.completed}}completed{{/}} {{if _page.edit.id === #todo.id}}editing{{/}}">
<div class="view">
<input class="toggle" type="checkbox" checked="{{#todo.completed}}">
<label on-dblclick="editTodo(#todo)">{{#todo.text}}</label>
<button class="destroy" on-click="delTodo(#todo.id)"> </button>
</div>
<form on-submit="doneEditing(_page.edit)">
<input id="{{#todo.id}}" class="edit" value="{{_page.edit.text}}" on-keyup="cancelEditing($event)">
</form>
</li>
{{/each}}
</ul>
app.proto.editTodo = function(todo){
this.model.set('_page.edit', {
id: todo.id,
text: todo.text
});
window.getSelection().removeAllRanges();
document.getElementById(todo.id).focus()
}
app.proto.doneEditing = function(todo){
this.model.set('todos.'+todo.id+'.text', todo.text);
this.model.set('_page.edit', {
id: undefined,
text: ''
});
}
app.proto.cancelEditing = function(e){
// 27 = ESQ-key
if (e.keyCode == 27) {
this.model.set('_page.edit.id', undefined);
}
}
При двойном щелчке срабатыват функция editTodo, в ней мы запоняем _path.edit, снимаем лишнее выделение, переключаем фокус на нужный нам input (здесь я немножко схитрил, дав input-у id = todo.id
).
После окончания редактирования, жмем либо enter, либо esq. Соответственно срабатывает один из двух обработчиков: doneEditing
, cancelEditing
. Изучите код — ничего нового.
Итак, последнее что мы сделаем — это выведем количество активных и выполненных задач в футере. Это хороший повод для того, чтобы объяснить, что такое реактивные функции.
$limit
, $skip
, $orderby
). Можно так же делать запросы, возвращающие количество элементов в коллекции (с какими-нибудь отборами) — это как раз наш случай. «Живые» запросы мы изучем в одном из следующих постов, сейчас же я посчитал уместным показать реализацию через реактивные функции, которые тоже часто используются в реальных приложениях.
Итак, рективная функция — это функция, которая срабатывает каждый раз при изменении каких-то данных. То есть мы должны указать, что вот эта конкретная реактивная функция будет следить за изменением вот этих конкретных данных. Эти данные приходят в эту функцию в качестве параметров. Далее она что-то вычисляет и возвращает результаты. Ее результаты привязывются к какому-то определенному «пути»…
Ладно, это все абстрактно и поэтому тяжело для восприятия. Давайте на нашем примере. У нас есть коллекция todos с активными и выполненными задачами. Хорошо бы, чтобы при любом изменении коллекции, нам, где-нибудь (например, по пути _page.counters
), были доступны счетчики активных и выполенных задач. Что-то типа:
_page.counters = {
active: 2,
completed: 3
}
Тогда бы мы смогли легко вывести эти данные в футер.
Один из вариантов получить данные счетчики — использовать реактивные функции. Регистрируются они так же, как и фильтры:
app.on('model', function(model) {
model.fn('all', function(item) { return true; });
model.fn('completed', function(item) { return item.completed;});
model.fn('active', function(item) { return !item.completed;});
model.fn('counters', function(todos){
var counters = { active: 0, completed: 0 };
for (var id in todos) {
if(todos[id].completed) counters.completed++; else counters.active++;
}
return counters;
})
});
Вот как мы зарегистрировали функцию counters
, но это еще не все. Ее еще нужно запустить в нужный момент и привязать к путям. Это делается в контроллере, при помощи функции model.start
:
model.subscribe('todos', function () {
model.filter('todos', filter).ref('_page.todos');
model.start('_page.counters', 'todos', 'counters');
page.render();
});
Все, теперь счетчики доступны в наших шаблонах. Дорабатываем футер:
<footer:>
<footer id="footer">
<span id="todo-count"><strong>{{_page.counters.active}} </strong>
<span>items left</span>
</span>
<ul id="filters">
<li><a href="/" class="{{if $render.url==='/' }}selected{{/}}">All</a></li>
<li><a href="/active" class="{{if $render.url==='/active' }}selected{{/}}">Active</a></li>
<li><a href="/completed" class="{{if $render.url==='/completed'}}selected{{/}}">Completed</a></li>
</ul>
<button id="clear-completed" on-click="clearCompleted()" class="{{if _page.counters.completed==0}}hidden{{/}}">
Clear completed ({{_page.counters.completed}})
</button>
</footer>
Показали нужные счетчики, попутно скрыв кнопку «Clear completed», если завершеных задач нет. Так же добавили класс selected
той ссылке, которая активна, воспользовавшись информацией полученной в процессе изучения app.model.get()
в консоли браузера. Да, зарезервированная коллекция $render
содержит различную полезную информацию, в частности url
, по которому шел рендеринг. Загляните в консоль еще раз.
Давйте поиграимся с тем, что получилось, откроем несколько вкладок, проверим, что все синхронизируется:
Проект на github [10], на случай, если захотите сравнить код.
P. S.
Если не хотите пропустить следующие статьи по derbyjs, подписывайтесь на обновления в моем профиле: zag2art [11]. Сам так делаю — на хабре же нет возможности добавить в трекер определенных (очень интересный) хаб, чтобы точно ничего не пропустить.
Автор: zag2art
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/59548
Ссылки в тексте:
[1] здесь: http://habrahabr.ru/post/221027/
[2] TodoMVC: http://todomvc.com/
[3] сделанный на Angular: http://todomvc.com/architecture-examples/angularjs/#/
[4] derby: http://derbyjs.com/
[5] css-файл: http://todomvc.com/architecture-examples/angularjs/bower_components/todomvc-common/base.css
[6] bg.png: http://todomvc.com/architecture-examples/angularjs/bower_components/todomvc-common/bg.png
[7] изоморфное: http://habrahabr.ru/post/203444/
[8] сами: https://github.com/codeparty/derby-starter/blob/master/lib/server.js#L72-L81
[9] http://localhost:3000/: http://localhost:3000/
[10] github: https://github.com/zag2art/derby-example-todo.git
[11] zag2art: http://habrahabr.ru/users/zag2art/
[12] Источник: http://habrahabr.ru/post/221703/
Нажмите здесь для печати.