пятница, 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.

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

1 комментарий:

  1. Рабочий пример. Но читаеть как простой пример не получается - нужно вникать.

    А так все отлично. Пиши еще.

    ОтветитьУдалить