понедельник, 23 января 2012 г.

Цепляем jQuery Datepicker c помощью кастомного биндинга на KnockoutJS

Как я говорил в предыдущей статье сегодня мы создадим свою "команду" ну или правильному custom binding (кастомный биндинг).
Что это такое ?
В knockout для того, чтобы с вашим элементом происходили какие-то махинации: скрыть, сделать неактивным, добавить стиль, присвоить значение и тп существуют команды, некоторые из которых вы уже видели в примерах - value, text, visible, ckecked, enable, options. Эти команды предопределены в фреймворке и предоставляют базовый и достаточный функционал для реализации ваших нужд.
Иногда бывают случаи когда код выглядит когда их вроде бы не хватает. Например, обавим в наши фильтры поддержку типа Date и когда пользователь выбрал поле, например Issue Date, установил курсор чтоб ввести значение, я хочу чтоб он не вводил его руками, а выбрал дату через jQuery datepicker. Естественно я хочу чтоб выбор даты срабатывал только для Date типа фильров, а для других Number и String оставался просто полем ввода.
Без своей кастомной команды, нам бы пришлось подписаться через subscribe на ColumnObj и смотреть при выборе поля из выпадающего списка его тип. Если это дата - применить datepicker, если нет - удалить datepicker. Если мы в будущем захотим добавить еще какую-то логику, то пойдут кастыли и будет все в куче. 
Тут нам как раз и поможет кастомный биндинг (команда). В итоге наш шаблон будет выглядеть так:
 
    <input data-bind="value: Value, datePicker: Type" type="text" />

Вся логика для выбора даты будет вынесена в команду datePicker. Собственно код:

ko.bindingHandlers.datePicker = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var type = ko.utils.unwrapObservable(valueAccessor());

        if (type == 'Date') {
            $(element).datepicker();
        }

    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var type = ko.utils.unwrapObservable(valueAccessor());

        if (type == 'Date') {
            $(element).datepicker();
        }
        else {
            $(element).datepicker("destroy");
        }
    }
}


Я не буду описывать все опции в деталях, о них выможете прочитать здесь.
Рассмотром, что и как.  Как мы видим для того чтобы создать команду, мы создаем объект с соответствующим называнием команды в ko.bindingHandlers. У нашего объекта должно быть две функции.

init - вызывается, когда knockout доходит до места в html, где установленна наша команда.
update - вызывается, когда изменится значение поля за которым мы следим с помощью нащей команды.
В нашем случае, data-bind="dataPicker: Type, value: Value" значит - следить за полем Type и если оно изменится вызвать команду datePicker. Она вызовится при инициализации и после последющих обновлений.
Что происходит в обработчиках?

valueAccessor() - возвращает поле, к которому привязана команд. Это всегда будет функция. т.к. все поля объявляются через ko.obsorvable или ko.computed и они возврашают функции.
ko.utils.unwrapObservable - это функция, которая вычисляет значение поля на такущий момент, когда вызывается эта строка.
Дальше, я думаю, все понятно. Параметры, которые передаютися в init и update очевидны, кроме allBindingsAccessor, но и пользоваться им вы врядли будете.

Ниже рабочий пример. Я добавил тип Date в Conditions, саму команду datePicker и поле Issue Date.

пятница, 13 января 2012 г.

Динамические фильтры на Javascript c помощью KnockoutJS

Сегодня я покажу практический пример, как можно применить KnockoutJS в реальном проекте и сделаем мы динамические фильтры для поиска задач, багов, тасков и тп по разработчику, названию таска и описанию. Возможно вы такие уже видели в TFS для генерации отчета или других системах, где можно добавлять любые фильтры и условия для поиска чего-либо. В итоге выглядит это так:



В конце мы добавим валидацию, чтоб нельзя было добавлять фильтр, если текущий пустой, а в следующей статье я напишу о том как можно писать плагины и свои команды в knockout и добавит фильтр по дате.

Приступим!

Сначала  определимся, что будет представлять из себя сам фильтр:


Column - колонка по кторой фильтруем или ищем
Condition - оператор для фильтра =, != и тп
Value - значение фильтра
Type - тип. Пока что у нас будет только String, Number потом расширим
NextFilterBinaryCondition - это условие с которым будет выполняться следующий фильтр "И", "ИЛИ". Например Developer = "Vitaly" and Issue = "write post" or Issue Description = "knockout".

Фильтры у нас будуте создаваться по названию поля по которому мы фильтруем. Колонки у нас хранятся как объекты. При создании фильтра мы сохраним название поля и найдем в списке объект колонку с помощью вспомогательной функции findFilterColumn и будем хранить обьъект колонка в поле ColumnObj. 
Если поле ColumnObj изменится, мы очищаем значение фильтра. Чтоб следить за изменениями поля ColumnObj мы подписуемся функцией subscribe.
Функцию-оброботчик, которыю мы передаем в subscribe, будет вызываться всегда, когда значение поля, на которое мы подписываемся, изменится.
Все поля, кроме ColumnObj, Condition, Value, NextFilterBinaryCondition  зависымые и значения мы не присваиваем им явно. Значения для этих полей пересчитываются при любом изменения одного из обычных полей. Это одна из прелестей knockout! Если смотреть сверху вниз то экосистема knockout будет вызывать обработчики в следующем порядке:
1. Присвоить значение в ColumnObj
2. Определеить поле Column как зависимое и попробовать просчитать.
3. Присвоить значение полю Condition.
4. Пересчитать Column
5. Присвоить значение полю NextFilterBinaryCondition.
6. Пересчитать Column.
7. Определеить поле Type как зависимое и просчитать.
8. Пересчитать Column.
9. Определеить поле AvailableConditions как зависимое и просчитать.
10. Пересчитать Type.
11. Пересчитать Column.
12. Установить значние поля Value и пересчитать Column, Type, AvailableConditions.

Все поля устанавливаются и пересчитываются по порядку в котором они были объявлены.

Теперь рассмотрит класс, который управляет всей бизнес логикой, хранит фильтры и он является и называется ViewModel

Объект Conditions содержит список типов фильтров и соответсвенно операций, условий, которые мы поодерживаем для этих типов данных. Также есть вспомогательная функция для поиска доступных условий по названию типа.

Например найти все доступные условия для фильтрации по типо 'Number'. Этой функцией мы будем пользоваться когда после выбора поля в первом списке, нам нужно будет отобразаить доступные условия во втором выпадающем списке.

Небольшое ТЗ. У нас должны выполнятся следующие требования:
- хранить фильтры
- добавлять фильтры
- удалять фильтры
- посылать все фильтры на сервер

При создании ViewModel мы передаем в него список колонок, которы объявлены выше. В конструкторе мы создаем массив фильтров и сразу добавляем один пустой фильтр. Если этого не сделать, то у нас на экране будет одна кнопка. Как только мы добавляем пустой фильтр, экосистема knockout трекает это, уведомляет всех подписчиков на изменения, если есть такие, и отображает пустой добавленные фильтр по шаблону и определенной для него бизнес логике.

Функция addFilter принимает объект фильтр, напротив которого мы клацнули "+", находит этот фильтр в списке уже существующих фильтров и добавляет после него новый пустой.
В данном случае у нас нет проверки, заполнены ли все фильтры или нет. Сейчас мы можем нажимать "+" бесконечно и у нас будет куча фильтров. Мы это исправим потом.

Функция removeFilter принимает объект фильтры который надо удалить, удаляет его из списка фильтров и если это был последний фильтр мы добавляем пустой. Зачем? Если мы этого не сделаем, то мы не сможем добавить фильтры, т.к. "+" отображается на строке с фильтром и без него у нас останется только кнопка submit.

Функция postFilters показывает сообщение с сериализованными фильтрами в JSON, которые надо послать на сервер для обработки. В knockout есть много утилитных функций о которых мы можете почитать подробней здесь.

Функция toJS, создает новый js объект, который не связан больше с knockout, это просто js объект. Все свойства в нем будут вычеслены с теми значениями, которые были на момент вызова функции toJS.

Когда мы сериализовали фильтры в js objects, то там будут поля, которые нам нужны и которые были нужны только для knockout, например ColumnObj, AvailableConditions. Эти поля не несут нам никакой информации на сервере, поэтому мы их смело удаляем и на сервер пойдет меньше данных. Функция toJSON сериализует js класс в JSON строку, которую мы и показываем.

Переходим к рассмотрению шаблона html:

Фильтры отображаются по шаблону в данном случае шаблон, это li и все что внутри него.
В предыдущих версия knockout обязательно надо было подключать jQuery Teample. В последней версии этого делать больше не нужно, теперь в фреймворке есть свой встроеный движок для шаблонов со своими плюшками. О всех новшествах новой версии 2.0 можно прочитать на блоге самого разработчика здесь.
Из нового  я использую переменную $root, которая ссылается на ViewModel класс. Если этого не сделать, то, например, поле columns будет искаться в классе Filter. Команда foreach пробегает по всем объектам коллекции filters и применяет шаблон для каждого объекта Filter. Шаблон выполняется с контекстом объекта, который передается в шаблон (Filter).
Переменная $data хранит ссылку на объект контекста (Filter в данном случае). Этой переменной я воспользовался, когда добавлял или удалял фильтр.
По умолчанию функции вызываются в контексте объекта в шаблоне (Filter), для этого в новой версии не нужно писать function () { ... }, есть более красиывый синтаксис например click: removeFilter($data). Но нам надо выполнять функции в контексте ViewModel поэтому мы пишем по старому, через function и выполняем функцию в контексте, котором нам надо.

Вроде бы все.

Теперь, как я говорил, добавим проверку. Если у нас хотя бы один фильтр без значения, то мы не будем добавлять новый.
Обновленный класс ViewModel выглядит теперь так:

а фильтр так:


и собственно результат:


В следующей статье я напишу как писать расширения для knockout и мы сделаем фильтр по дате. Если у нас поле будет с типом Date, то к нему будет автоматически применяться jQuery date picker.

Вопросы, коментарии преветсвуются!