Изучаем Derby 0.6, пример #2
Этот пост продолжение серии, начатой здесь. Сегодня мы создадим, так называемый, «список дел» (Todo-list из проекта TodoMVC). За основу возьмем варинт, сделанный на Angular, и попробуем воссоздать функционал на derby.
Исследуем рабочий вариант
Итак, посмотрим, что у нас есть в ангуляровском варианте, и как оно работает (потратьте минут 5, чтобы разобраться в функционале): новая задча вводится в верхнем поле ввода, в список попадает при нажатии на enter;
любую задачу в списке можно удалить, кликнув на «крестик» справа от задачи (появляется, если навести мышку на задачу);
задачи можно помечать как «выполненные», кликнув на «галочку» слева от задачи (отметку можно снимать);
при двойном клике мышкой на задаче, она переходит в режим редактирования, привим, жмем enter — она обновляется;
если у нас есть выполненные задачи, справа снизу появляется кнопка «clear completed», если нажать на нее выполненные задачи удалятся;
ведется подсчет (и отображается) выполненных и активных задач. Снизу в статусной строке;
так же снизу, в статусной строке есть 3 ссылки (all, active, completed, меняющие url на '#/', '#/active' и '#/completed' соответственно), кликнув по ним мы меняем фильт задач: либо отображаются все задачи, либо только активные (не выполненные), либо только выполненные;
Что возьмем за основу
Исходя из наших целей (узнать лучше derbyjs), мы не будем здесь придумывать стили, они уже написаны и используются без изменений в большинстве реализаций TodoMVC. Просто возьмем css-файл. Беглый взгляд по нему показывает, что нам нужно будет взять еще и картинку для фона bg.png. Так же, в качетстве каркаса возьмем сгенерированный ангуляром html (я скопировал его с помощью инструментов разработчика в браузере и немного почистил от ангуляровских деректив).Базовый html-код
todos
Вообще (на будущее), можно разбивать проект на несколько дерби-приложений, например, клиентская часть и админка. Это оправдано по двум причинам, чтобы не отдавать лишние данные (шаблоны, стили, код), и чтобы уменьшить связанность. То есть будет так: в проекте будет одна серверная часть и несколько дерби-приложений (в данном случае два).
В файле package.json в качестве зависимостей будут все те же два модуля: derby@0.6.0-alpha5 и derby-starter.
Начинаем Создем файловую структуру. Фоновую картику и стили качаем по ссылкам, которые я указал вначале, package.json создаем при помощи npm init (можете посмотреть в предыдущем уроке).Html немножко подправим, во-первых, как и в предыдущем примере, он должен находиться в предопределенном шаблоне Body:, во-вторых вынесем header, main и footer в отдельные derby-шаблоны.
Итоговый index.html
todos
Как вы могли заметить, вызов собственных шаблонов происходит при помощи тега view, где в атрибуте name, задается имя шаблона.Для начала, создадим минимальный работающий код, чтобы иметь возможность видеть в браузере результат и наращивать функционал.
Файл server.js из прошлого примера немного расширен, чтобы учесть структуру проекта и отдавать статические файлы.
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. Посмотрите сами.Минимальный index.js: 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/ результат:
Стили с версткой подцепились. Начало положено.
Проектируем url В прошлом уроке я говорил, что дерби-разработчик должен начинать разработку с разбиения проекта на url-адреса. Это связано с возмжностью дерби генерировать страницы как на клиенте так и на сервере, что очень любят поисковые системы. Итак, изучая ангуляровский вариант мы заметили, что в футере есть 3 ссылки, меняющие url и соответственно фильтр по задачам. Здесь мы понимаем, что у нас в приложении должно быть 3 обработчика get-запросов. Что-то типа: app.get ('/', getAllTodos); app.get ('/active', getActiveTodos); app.get ('/completed', getCompletedTodos); Это было бы оправданно, если бы все эти страницы были разными, но у нас, единственное отличение между ними — фильтр, поэтому постараемся по минимуму дублировать код.Проектируем данные Сами задачи у нас будут храниться в коллекции todos. Каждая задача будет представленна двумя полями: text — описание задачи completed — признак того, что задача выполнена К этому нужно добавить, что у каждой задачи еще, конечно же, будет поле id — derby добавит его автоматически при добавлении элемента в коллекцию.Итак, в соответствии с методологией дерби, в контроллере (функции, обрабатывающей запрос к url) до вызова render мы должны подготовить данные и зарегистрировать подписки на обновление данных. Получается обработчик, схематично должен быть примерно таким:
function getTodos (page, model){ model.subscribe ('todos', function (){ page.render (); }); } Так примерно и будет, но прежде, чем двигаться дальше (к тому чтобы сделать один контроллер для всех трех запросов, только чтобы фильтры по задачам были разные) нужно узнать несколько вещей о моделях дерби: «пути», начинающиеся с символа подчеркивания (например,»_session»,»_page» и т.д.) в чем особенность »_page» что такое в дерби фильтры что такое ref к определенным данным в коллекции В прошлом уроке я говорил о так-называемых «путях». Мы исопльзуем их в операциях с моделями. Например при подписке на данные: model.subscribe ('путь'), при получении и при записи данных в модель: model.get ('путь'), model.set ('путь', значение). Примеры путей:'todos' — ссылаемся на всю коллекцию todos 'users.42' — ссылаемся на запись в коллекции users c id = 42 Так вот. Первый сергмент пути — это, как вы поняли, имя коллекции. Это имя в дерби может начинаться либо с латинской буквы, либо с сиволов $ или _. Все коллекции, начинающиеся с $ и _ особенные, они не синхронизируются с сервером (являются локальными для модели, а модель в приложении-дерби создается всего одна). Коллекции начинающиеся с $ зарезервированы дерби для собственных нужд. Коллекции же, начинающиеся с символа подчеркивания используются разработчиками.Давайте, проведем небольшой эксперимент. Откройте в браузере консоль разработчика и наберите 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», но думаю это не большая плата за отсутствие дублей.Ладно мы немножно отвлеклись. Давайте оживим приложение.
Добавление и вывод задач Инпут для ввода данных в верстке у нас выглядет так:
Классический паттерн в дерби (как и во многих современных фреймворках) — рективное связывание. Свяжем значение, вводимое в input, с каким-нибудь путем в _page. Так же зарегистрируем обработчик события submit формы, для того, чтобы обрабатывать нажание на enter: Вместо, 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-элемент:
-
{{each _page.todos as #todo, #index}}
-
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 редактируемой задачи, и текст.
Зачем хранить текст отдельно, он же у нас уже хранится в самой задаче? Все зависит от целей. Если бы мы связали с input-ом текст напрямую из задачи, то пользователь редактировал бы элемент напрямую в базе данных. То есть его правки (каждое нажатие на кнопку) мгновенно бы показывалось у других пользователей в браузере. Более того, несколько пользователей одновременно могли бы править текст и видеть все изменения, но это не то, что нам нужно. Обычным сценарием является фиксация в базе окончательно отредактированных данных, либо отказ от фиксации… То есть у всех все должно обновлятья только тогда, когда пользователь нажал на enter.
Итак, реализуем все это:
-
{{each _page.todos as #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. Изучите код — ничего нового.
Количество активных и выполненных задач — реактивные функции Итак, последнее что мы сделаем — это выведем количество активных и выполненных задач в футере. Это хороший повод для того, чтобы объяснить, что такое реактивные функции.Небольшая ремарка на счет архитектуры проекта Следует отметить, что тот вариант реализации приложения, который я выбрал не является единственным. Обдумывая данный, конкретный проект с ходу приходит на ум использование live-query — это еще один офигенный механизм дерби, позволяющий сделать mongo-запрос в базу данных, результаты которого будут реаткивно обновляться. В запросах, конечно же, можно использовать различные отборы, сортировки, органичения по количеству ($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 (); }); Все, теперь счетчики доступны в наших шаблонах. Дорабатываем футер:
Проект на github, на случай, если захотите сравнить код.
P.S. Если не хотите пропустить следующие статьи по derbyjs, подписывайтесь на обновления в моем профиле: zag2art. Сам так делаю — на хабре же нет возможности добавить в трекер определенных (очень интересный) хаб, чтобы точно ничего не пропустить.