Статьи

На ринг с Knockout.js: титульный бой

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

Одной из функций, которые мы добавим в этой части, является возможность фильтровать отображаемый список контактов по первой букве их имени — довольно распространенная функция, которую может быть сложно выполнить вручную. Кроме того, читатель второй части этой серии спросил, насколько сложно было бы добавить функцию поиска с помощью Knockout, поэтому мы также добавим поле поиска в пользовательский интерфейс, которое позволит подмножеству контактов, которые соответствуют определенному поисковому запросу, отображаться. Давайте начнем.


Мы начнем с добавления новой разметки в представление. В файле index.html из предыдущего урока добавьте следующую новую разметку в начало <body> :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
    <div id=»alphaFilter»>
<span>Filter name by:
<ul data-bind=»template: ‘letterTemplate'»></ul>
<a id=»clear» href=»#» title=»Clear Filter» data-bind=»click: clearLetter, css: { disabled: filterLetter() === » }»>Clear filter</a>
<fieldset id=»searchForm»>
    <span>Search for:
    <button data-bind=»click: setTerm, disable: filterTerm» type=»button»>Go</button>
    <input id=»term»>
    <a data-bind=»visible: filterTerm, click: clearTerm» title=»Clear search» href=»#»>x</a>
</fieldset>
</div>
<script id=»letterTemplate» type=»text/x-jquery-tmpl»>
{{each(i, val) letters}}
    <li>
        <a href=»#» title=»Filter name by ${ val }» data-bind=»click: function() {
            filterLetter(val) },
            css: { disabled: val === filterLetter() }»>
            ${ val }
        </a>
    </li>
{{/each}}
</script>

Мы начнем с простого внешнего контейнера для хранения наших новых элементов пользовательского интерфейса, которым мы даем id для стилизации. Внутри находится <span> содержащий пояснительную метку для букв, используемых для фильтрации контактов по имени, за которым следует пустой элемент <ul> который мы привязываем к шаблону letters с помощью атрибута data-bind .

После списка есть ссылка; эта ссылка используется для очистки фильтра и имеет две привязки: первая — привязка click , которая связана с методом в нашей viewModel который мы добавим через мгновение. Второй привязкой является привязка css , которая используется для добавления имени класса, disabled к элементу, когда буква фильтрации не была выбрана.

Компонент поиска нашего пользовательского интерфейса использует <fieldset> с id (также для стиля), который содержит пояснительную текстовую метку, элемент <button> который будет запускать поиск, <input> , в который будет введен поисковый термин, и ссылка, которая может быть использована для очистки поиска.

<button> использует click и disable привязки; привязка click используется для запуска поиска, а disable привязки отключит кнопку, когда значение filterTerm равно пустой строке (что равно false ). Ссылка очистки также имеет две привязки: visible и click . visible привязка используется для отображения ссылки только после выполнения поиска, а привязка click используется для очистки поиска.

Затем мы добавляем шаблон letters jQuery, который используется для создания писем, используемых для фильтрации по первой букве имени каждого контакта. Как и в случае с числовой подкачкой из предыдущего урока, мы используем здесь синтаксис jQuery tmpl вместо tmpl функциональности Knockout. Это означает, что весь шаблон будет перерисован при изменении одного из элементов, но в этом примере это не сильно влияет на производительность.

Мы используем шаблонный тег {{each}} и будем использовать второй параметр val , который передается шаблону для каждого элемента в массиве, который использует шаблон, который будет соответствовать первой букве имени каждого контакта (мы скоро мы увидим, как этот массив генерируется, когда мы обновим нашу viewModel ).

Для каждого элемента в array мы создаем элемент <li> и <a> . Элемент <a> использует параметр val переданный в функцию шаблона, для установки атрибута title ссылки и ее текстового содержимого. Мы также добавляем привязки click и css . Привязка click устанавливает для filterLetter viewModel (которое будет filterLetter viewModel для наблюдения) значение ссылки, по которой был выполнен щелчок. Привязка css просто добавляет disabled класс так же, как мы делали с очищением <a> , но на этот раз класс применяется, если значение текущего элемента равно filterLetter .

Хотя вы не сможете запустить страницу в этот момент, компоненты фильтрации и поиска будут выглядеть так, как только необходимый код будет добавлен в viewModel :


Чтобы соединить элементы, которые мы только что добавили, нам сначала нужно добавить некоторые новые свойства и методы в нашу viewModel . Они могут идти после метода navigate из последней части руководства (не забудьте добавить запятую после navigate ):

01
02
03
04
05
06
07
08
09
10
11
12
filterLetter: ko.observable(«»),
filterTerm: ko.observable(«»),
clearLetter: function () {
  this.filterLetter(«»);
},
clearTerm: function () {
  this.filterTerm(«»);
$(«#term»).val(«»);
},
setTerm: function () {
  this.filterTerm($(«#term»).val());
}

Нам также понадобятся несколько новых dependentObservables наблюдаемых dependentObservables , но мы добавим их чуть позже. Сначала мы добавляем два новых наблюдаемых свойства: filterLetter , который используется для отслеживания текущей буквы, по которой выполняется фильтрация, и filterTerm , который отслеживает текущий поисковый filterTerm . Оба по умолчанию настроены на пустые строки.

Далее мы добавим несколько методов; первый метод, clearLetter , устанавливает наблюдаемый filterLetter обратно в пустую строку, которая очищает фильтр, а второй метод, clearTerm , устанавливает наблюдаемый filterTerm обратно в пустую строку, которая очищает поиск. Этот метод также удалит строку, введенную в текстовое поле в представлении. Последний новый метод, setTerm , используется для получения строки, введенной в текстовое поле, и добавления ее в наблюдаемый filterTerm .


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
viewModel.filteredPeopleByTerm = ko.dependentObservable(function () {
var term = this.filterTerm().toLowerCase();
 
if (!term) {
    return this.people();
}
 
return ko.utils.arrayFilter(this.people(), function (person) {
    var found = false;
 
    for (var prop in person) {
        if (typeof (person[prop]) === «string») {
            if (person[prop].toLowerCase().search(term) !== -1) {
                found = true;
                break;
            }
        }
    }
 
    return found;
});
 
}, viewModel);

В рамках функции мы сначала сохраняем поисковый термин в нижнем регистре, чтобы при поиске не учитывался регистр. Если поисковый термин равен false (если это пустая строка), функция вернет array people . Если есть arrayFilter() поиска, мы используем функцию утилиты arrayFilter() Knockout для фильтрации array people . Эта служебная функция принимает array для фильтрации и анонимную функцию, которая будет выполняться для каждого элемента в фильтруемом array .

В нашей анонимной функции мы сначала устанавливаем переменную flag в false . Затем мы перебираем каждое свойство, которое содержит текущий элемент array . Мы проверяем, что текущее свойство является строкой, и если так, мы определяем, содержит ли свойство поисковый термин. Это делается путем преобразования свойства в нижний регистр и последующего использования встроенного в JavaScript метода search() . Если метод search() не возвращает -1 , мы знаем, что совпадение найдено, и поэтому мы устанавливаем для нашей переменной-флага значение true и выходим из цикла for с помощью оператора break .

После того, for цикл for завершен (или мы вырвались из него с совпадением), переменная flag будет возвращена и будет иметь значение true или false . arrayFilter метод arrayFilter будет включать элементы из исходного массива в массив, который он возвращает, если анонимная функция, выполненная для каждого элемента, возвращает true . Это обеспечивает простой механизм возврата подмножества массива people для использования другими dependentObservables .


Наш следующий dependentObservable объект используется для создания массива букв, который использует шаблон letters для добавления буквенных ссылок в пользовательский интерфейс:

1
2
3
4
5
6
7
8
9
viewModel.letters = ko.dependentObservable(function () {
var result = [];
 
ko.utils.arrayForEach(this.filteredPeopleByTerm(), function (person) {
    result.push(person.name.charAt(0).toUpperCase());
});
 
return ko.utils.arrayGetDistinctValues(result.sort());
}, viewModel);

В этом dependentObservable мы сначала создаем пустой массив с именем result . Мы используем arrayForEach утилиты arrayForEach Knockout для обработки каждого элемента в массиве, возвращенного предыдущим dependentObservable arrayForEacharrayForEach . Для каждого элемента мы просто помещаем первую букву свойства name каждого элемента в верхнем регистре в массив result . Затем мы возвращаем этот массив после прохождения его через метод утилиты Knockout arrayGetDistinctValues() и сортируем его. Используемый здесь служебный метод фильтрует массив и удаляет любые дубликаты.


Последняя dependentObservable наблюдательная filterLetter нам нужно добавить, фильтрует контакты по буквам и запускается, когда наблюдаемая filterLetter меняет значение:

01
02
03
04
05
06
07
08
09
10
viewModel.filteredPeople = ko.dependentObservable(function () {
    var letter = this.filterLetter();
    if (!letter) {
        return this.filteredPeopleByTerm();
    }
 
    return ko.utils.arrayFilter(this.filteredPeopleByTerm(), function (person) {
        return person.name.charAt(0).toUpperCase() === letter;
    });
}, viewModel);

В этом dependentObservable filterLetter мы сначала сохраняем содержимое объекта filterLetter наблюдаемого в массиве. Если переменная letter равна false (например, если это пустая строка), мы просто возвращаем массив, который возвращает метод filteredPeopleByTerm() не изменяя его.

Если есть буква для фильтрации, мы снова используем метод утилиты arrayFilter() чтобы отфильтровать массив, возвращаемый filteredPeopleByTerm arrayFilter() . На этот раз мы конвертируем первую букву свойства name каждого элемента в верхний регистр и возвращаем, равно ли оно букве. Помните, что элементы останутся в массиве, который мы фильтруем, только если анонимная функция вернет true .


В последнем уроке этой мини-серии мы добавили функцию подкачки, которая работала непосредственно с массивом people . Если мы хотим, чтобы подкачка работала с нашей новой функцией фильтрации, нам нужно обновить showCurrentPage dependentObservable в предыдущей статье. Все, что нам нужно сделать, — это изменить оператор return в конце функции, чтобы он возвращал часть массива, возвращаемого filteredPeople() dependentObservable вместо массива people :

1
return this.filteredPeople().slice(startIndex, startIndex + this.pageSize());

На этом этапе мы теперь можем запустить страницу и отфильтровать отображаемые контакты по букве или поисковому запросу. Эти две новые функции не являются взаимоисключающими, поэтому мы можем отфильтровать контакты по букве, а затем выполнить поиск в отфильтрованном списке с помощью поискового запроса. Или наоборот — фильтрация искомого набора контактов. И наш пейджинг будет по-прежнему соответствовать текущему набору контактов.


В этой последней главе этой серии мы объединили то, что мы знаем об использовании Knockout, добавив функции фильтрации по буквам или поисковым словам, чтобы пользователи могли просматривать подмножество данных, viewModel в viewModel . Как и раньше, добавление этих новых функций в наше приложение намного проще сделать с Knockout, чем если бы мы пытались поддерживать наши представления и viewModels вручную, используя только jQuery.

Наряду с простотой синхронизации нашего пользовательского интерфейса и данных, мы также получаем ряд служебных функций, включая arrayGetDistinctValues() и arrayFilter() которые мы можем использовать, чтобы сэкономить некоторое ручное кодирование при выполнении обычных задач.

Это подводит нас к концу серии, но я надеюсь, что это не конец вашего опыта с самим нокаутом; библиотека является фантастическим дополнением к инструментарию любого разработчика и значительно ускоряет создание интерактивных приложений с использованием JavaScript.