Статьи

Создайте менеджер контактов с помощью Backbone.js: часть 2

Добро пожаловать ко второй части этого урока; В первой части мы рассмотрели некоторые основы модели, коллекции и представления при работе с Backbone и увидели, как визуализировать отдельные виды контактов с использованием основного представления, привязанного к коллекции.

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


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

Теперь мы можем жестко закодировать выбранное меню в наш базовый HTML и вручную добавить опции для каждого из различных типов. Но это не было бы очень дальновидным мышлением; что если мы добавим новый тип позже или удалим все контакты определенного типа? Наше приложение еще не имеет возможности добавлять или удалять контакты (часть третья, предупреждение о спойлере!), Но все же лучше принимать во внимание такие вещи, даже на этом раннем этапе нашего приложения.

Таким образом, мы можем легко построить элемент выбора динамически на основе существующих типов. Сначала мы добавим чуть-чуть HTML на нижележащую страницу; добавьте следующие новые элементы в контейнер контактов:

1
2
3
<header>
    <div id=»filter»><label>Show me:</label></div>
</header>

Вот и все, у нас есть внешний элемент <header> который действует как общий контейнер, внутри которого находится другой контейнер с атрибутом id , и элемент <label> с некоторым пояснительным текстом.

Теперь давайте <select> элемент <select> . Сначала мы добавим два новых метода в наше представление DirectoryView mater; первый извлечет каждый уникальный тип, а второй на самом деле построит раскрывающийся список. Оба метода должны быть добавлены в конец представления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
getTypes: function () {
    return _.uniq(this.collection.pluck(«type»), false, function (type) {
        return type.toLowerCase();
    });
},
 
createSelect: function () {
    var filter = this.el.find(«#filter»),
        select = $(«<select/>», {
            html: «<option>All</option>»
        });
 
    _.each(this.getTypes(), function (item) {
        var option = $(«<option/>», {
            value: item.toLowerCase(),
            text: item.toLowerCase()
        }).appendTo(select);
    });
    return select;
}

Первый из наших методов, getTypes() возвращает массив, созданный с использованием метода Underscore uniq() . Этот метод принимает массив в качестве аргумента и возвращает новый массив, содержащий только уникальные элементы. Массив, который мы передаем в метод uniq() генерируется с помощью метода uniq() Backbone, который представляет собой простой способ извлечь все значения одного атрибута из коллекции моделей. Интересующий нас атрибут является атрибутом type .

Чтобы избежать проблем с регистром позже, мы должны также нормализовать типы в нижний регистр. Мы можем использовать функцию итератора, предоставленную в качестве третьего аргумента для uniq() , для преобразования каждого значения перед его передачей через компаратор. Функция получает текущий элемент в качестве аргумента, поэтому мы просто возвращаем элемент в нижнем регистре. Второй аргумент, передаваемый uniq() , который мы установили в false , это флаг, используемый для указания того, был ли отсортирован сравниваемый массив.

Второй метод createSelect() немного больше, но не намного сложнее. Его единственная цель — создать и вернуть новый элемент <select> , поэтому мы можем вызвать этот метод где-то еще в нашем коде и получить новый блестящий раскрывающийся список с опцией для каждого из наших типов. Мы начнем с того, что дадим новому элементу <select элемент по умолчанию <option> с текстом All .

Затем мы используем метод each() Underscore для итерации каждого значения в массиве, возвращаемого нашим getTypes() . Для каждого элемента в массиве мы создаем новый элемент <option> , устанавливаем его текст в значение текущего элемента (в нижнем регистре), а затем добавляем его в <select> .

Чтобы фактически отобразить элемент <select> на странице, мы можем добавить некоторый код в метод initialize() нашего основного представления:

1
this.$el.find(«#filter»).append(this.createSelect());

Контейнер для нашего основного представления кэшируется в свойстве $el которое Backbone автоматически добавляет к нашему классу представления, поэтому мы используем его для поиска контейнера фильтра и добавления к нему элемента <select .

Если мы запустим страницу сейчас, мы должны увидеть наш новый элемент <select> с опцией для каждого из различных типов контактов:


Итак, теперь у нас есть <select menu, мы можем добавить функциональность для фильтрации представления при выборе опции. Для этого мы можем использовать атрибут events основного представления для добавления обработчика событий пользовательского интерфейса. Добавьте следующий код непосредственно после нашего renderSelect() :

1
2
3
events: {
    «change #filter select»: «setFilter»
},

Атрибут events принимает объект пары key:value где каждый ключ указывает тип события и селектор, к которому привязывается обработчик события. В этом случае нас интересует событие change которое будет #filter элементом <select внутри контейнера #filter . Каждое значение в объекте является обработчиком события, который должен быть связан; в этом случае мы указываем setFilter в качестве обработчика.

Далее мы можем добавить новый обработчик:

1
2
3
4
setFilter: function (e) {
    this.filterType = e.currentTarget.value;
    this.trigger(«change:filterType»);
},

Все, что нам нужно сделать в функции setFilter() — это установить свойство в главном представлении с именем filterType , для которого мы устанавливаем значение выбранного параметра, которое доступно через свойство currentTarget объекта события, которое автоматически передается нашему обработчику.

После того, как свойство было добавлено или обновлено, мы также можем вызвать для него событие пользовательского change используя имя свойства в качестве пространства имен. Мы рассмотрим, как мы можем использовать это пользовательское событие всего за мгновение, но прежде чем мы это сделаем, мы можем добавить функцию, которая фактически будет выполнять фильтр; после метода setFilter() добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
filterByType: function () {
    if (this.filterType === «all») {
        this.collection.reset(contacts);
    } else {
        this.collection.reset(contacts, { silent: true });
 
        var filterType = this.filterType,
            filtered = _.filter(this.collection.models, function (item) {
            return item.get(«type»).toLowerCase() === filterType;
        });
 
        this.collection.reset(filtered);
    }
}

Сначала мы проверяем, установлено ли для свойства filterType представления значение all ; если это так, мы просто заполняем коллекцию полным набором моделей, данные для которых хранятся локально в нашем массиве contacts .

Если свойство не равно all , мы по-прежнему сбрасываем коллекцию, чтобы вернуть все контакты обратно в коллекцию, что необходимо для переключения между различными типами контактов, но на этот раз мы устанавливаем для параметра без вывода сообщений значение true (вы Посмотрим, почему это необходимо в данный момент), чтобы событие reset не было запущено.

Затем мы сохраняем локальную версию свойства filterType чтобы мы могли ссылаться на него в функции обратного вызова. Мы используем метод filter() Underscore для фильтрации коллекции моделей. Метод filter() принимает массив для фильтрации и функцию обратного вызова для выполнения каждого элемента в фильтруемом массиве. В функцию обратного вызова передается текущий элемент в качестве аргумента.

Функция обратного вызова будет возвращать true для каждого элемента, который имеет атрибут type равный значению, которое мы только что сохранили в переменной. Типы снова преобразуются в нижний регистр по той же причине, что и раньше. Любые элементы, для которых функция обратного вызова возвращает false , удаляются из массива.

Как только массив был отфильтрован, мы reset() вызываем метод reset() , передавая отфильтрованный массив. Теперь мы готовы добавить код, который будет setType() метод setType() , свойство filterByType() и filterByType() .


Помимо привязки событий пользовательского интерфейса к нашему интерфейсу с помощью атрибута events , мы также можем привязывать обработчики событий к коллекциям. В нашем методе setFilter() мы запустили пользовательское событие, теперь нам нужно добавить код, который будет привязывать метод filterByType() к этому событию; добавьте следующий код в метод initialize() нашего основного представления:

1
this.on(«change:filterType», this.filterByType, this);

Мы используем метод Back on() Backbone для прослушивания нашего пользовательского события. Мы указываем метод filterByType() в качестве функции-обработчика для этого события, используя второй аргумент on() , а также можем установить контекст для функции обратного вызова, установив this в качестве третьего аргумента. Здесь this объект относится к нашему основному виду.

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

1
this.collection.on(«reset», this.render, this);

В этом случае мы прослушиваем событие reset и функция, которую мы хотим вызвать, является методом render() коллекции. Мы также указываем, что обратный вызов должен использовать this (как в случае основного представления) в качестве контекста при его выполнении. Если мы не предоставим this в качестве третьего аргумента, мы не сможем получить доступ к коллекции внутри метода render() когда он обрабатывает событие reset .

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


Итак, все, что у нас есть, в порядке, мы можем отфильтровать наши модели, используя поле выбора. Но разве не было бы замечательно, если бы мы могли фильтровать коллекцию, также используя URL? Модуль маршрутизатора Backbone дает нам эту возможность, давайте посмотрим, как, и благодаря тому, что мы до сих пор хорошо структурировали нашу фильтрацию, очень легко добавить эту функцию. Сначала нам нужно расширить модуль Router; добавьте следующий код после основного представления:

01
02
03
04
05
06
07
08
09
10
var ContactsRouter = Backbone.Router.extend({
    routes: {
        «filter/:type»: «urlFilter»
    },
 
    urlFilter: function (type) {
        directory.filterType = type;
        directory.trigger(«change:filterType»);
    }
});

Первое свойство, которое мы определяем в объекте, передаваемом в метод extend() маршрутизатора, — это routes , которые должны быть литералом объекта, где каждый ключ является URL-адресом для сопоставления, а каждое значение является функцией обратного вызова при сопоставлении URL-адреса. В этом случае мы ищем URL, которые начинаются с #filter и заканчиваются чем-то еще. Часть URL после filter/ части передается функции, которую мы указали в качестве функции обратного вызова.

В этой функции мы устанавливаем или обновляем свойство filterType основного представления, а затем снова запускаем наше событие пользовательского change . Это все, что нам нужно сделать, чтобы добавить функцию фильтрации по URL. Однако нам все еще нужно создать экземпляр нашего маршрутизатора, что можно сделать, добавив следующую строку кода непосредственно после создания DirectoryView :

1
var contactsRouter = new ContactsRouter();

Теперь у нас должна быть возможность ввести URL-адрес, такой как #filter/family и представление будет повторно отображаться, чтобы показать только контакты с типом family:

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

Для этого требуется два шага; Прежде всего мы должны включить поддержку истории Backbone, запустив службу истории после инициализации нашего приложения; добавьте следующую строку кода прямо в конец нашего файла сценария (непосредственно после инициализации нашего маршрутизатора):

1
Backbone.history.start();

С этого момента Backbone будет отслеживать URL-адреса на предмет изменений хеша. Теперь, когда мы хотим обновить URL после того, как что-то происходит, мы просто вызываем метод navigate() нашего маршрутизатора. Измените метод filterByType() чтобы он filterByType() так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
filterByType: function () {
    if (this.filterType === «all») {
        this.collection.reset(contacts);
 
        <b>contactsRouter.navigate(«filter/all»);</b>
 
    } else {
        this.collection.reset(contacts, { silent: true });
 
        var filterType = this.filterType,
            filtered = _.filter(this.collection.models, function (item) {
                return item.get(«type») === filterType;
        });
 
        this.collection.reset(filtered);
 
        <b>contactsRouter.navigate(«filter/» + filterType);</b>
    }
}

Теперь, когда поле выбора используется для фильтрации коллекции, URL-адрес будет обновлен, и пользователь сможет затем добавить в закладки или поделиться URL-адресом, а кнопки браузера «назад» и «вперед» будут перемещаться между состояниями. Начиная с версии 0.5 Backbone также поддерживает API pushState, однако, чтобы это работало правильно, сервер должен иметь возможность отображать запрашиваемые страницы, которые мы не настроили для этого примера, следовательно, используя стандартный модуль истории.


В этой части руководства мы рассмотрели еще пару модулей Backbone, в частности модули Router, History и Events. Теперь мы рассмотрели все различные модули, которые поставляются с Backbone.

Мы также рассмотрели еще несколько методов Underscore, включая filter() , которые мы использовали для фильтрации нашей коллекции только по тем моделям, которые содержат определенный тип.

Наконец, мы посмотрели на модуль Router Backbone, который позволял нам задавать маршруты, которые могут использоваться нашим приложением для запуска методов, и модуль History, который мы можем использовать для запоминания состояния и обновления URL-адреса с помощью хеш-фрагментов.

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

В следующей части этой серии мы вернемся к работе с моделями и увидим, как мы можем удалять модели и добавлять новые в коллекцию.