Фильтрация в AngularJS

В этом уроке мы добавим полнотекстовый поиск, а также напишем end-to-end тест, постоянное наблюдение за которым позволит вовремя заметить регрессию.


Переключимся на третий шаг:
$ git checkout -f step-3

По сравнению с предыдущим шагом мы внесли такие изменения.

Мы не тронули контроллер, он остался таким же как и на предыдущем шаге.

В app/index.html внесем следующие изменения:

<div class="container-fluid">
  <div class="row">
    <div class="col-md-2">
      <!--Sidebar content-->

      Search: <input ng-model="query">

    </div>
    <div class="col-md-10">
      <!--Body content-->

      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query">
          {{phone.name}}
          <p>{{phone.snippet}}</p>
        </li>
      </ul>

    </div>
  </div>
</div>

Тут мы добавили стандартный тег <input> и использовали функцию filter для обработки входных данных поступающих в директиву ngRepeat.

Data-binding одна из основных возможностей в Angular. Когда страница загружается, Angular связывает имя текстового поля с переменной с таким же именем в модели данных и держит обе эти вещи в синхронизированном состоянии.

В приведенном выше коде данные, которые вводит пользователь в текстовое поле (query) немедленно становятся доступными для фильтра в цикле списка (phone in phones | filter:query). Когда изменяется модель данных, то цикл прогоняется снова и обновляет DOM, чтобы отобразить текущее состояние модели данных.


Функция filter использует значение переменной query чтобы создать новый массив, который будет содержать только те записи, которых соответствуют query.

ngRepeat автоматически обновляет вьюху потому что меняется количество возвращаемых телефонов фильтром filter. Это происходит полностью незаметно для разработчика.

Чтобы лучше понять как работает привязка к переменной query попробуем отображать поисковый запрос в заголовке окна браузера. Для этого в файле app/index.html изменим тег заголовка на следующий:

<title>Google Phone Gallery: {{query}}</title>

Но этого недостаточно, т.к. "query" у нас живет в области видимости определяемой директивой ng-controller="PhoneListCtrl" в теге <body>. Поэтому надо перенести это объявление в тег <html>, потому что он содержит в себе оба тега и <title> и <body>.

<html ng-app="phonecatApp" ng-controller="PhoneListCtrl">

При такой реализации возникает следующий глюк (видны фигурные скобки):
Чтобы его убрать надо поменять тег <title> следующим образом:
  • <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
Для такого случая можно директивы ngBind или ngBindTemplate, которые невидимы пользователю пока загружается страница.

Тестирование

На прошлом шаге мы рассмотрели как запускать unit-тесты, они прекрасно подходят для тестирования контроллеров и других компонентов нашего приложения написанных на JavaScript. Но их нелегко использоваться для тестирования изменений в DOM. Для этой целей лучше подходят end-to-end тесты.

Возможность поиска мы реализовали полностью на шаблонах и на привязке к данным (data-binding). Сейчас мы напишем end-to-end тест, который проверяет, что все работает правильно.

describe('PhoneCat App', function() {

  describe('Phone list view', function() {

    beforeEach(function() {
      browser.get('app/index.html');
    });


    it('should filter the phone list as a user types into the search box', function() {

      var phoneList = element.all(by.repeater('phone in phones'));
      var query = element(by.model('query'));

      expect(phoneList.count()).toBe(3);

      query.sendKeys('nexus');
      expect(phoneList.count()).toBe(1);

      query.clear();
      query.sendKeys('motorola');
      expect(phoneList.count()).toBe(2);
    });
  });
});

Этот тест проверяет, что текстовое поле и цикл работают корректно в связке друг с другом.

Несмотря на то что синтаксис этого теста похож на синтаксис unit-теста, который мы писали ранее с помощью Jasmine, но end-to-end тесты используют API от Protractor.

Также как Karma используется для пуска unit-тестов, мы будем использовать Protractor для запуска end-to-end тестов:
npm run protractor

End-to-end тесты медленные, поэтому в отличие от модульных тестов, Protractor завершит свою работу после пуска теста и не будет автоматически тестировать каждое изменение файла. Для того чтобы повторить тест нужно снова запустить:
npm run protractor

Перед этим надо убедиться, что приложение запущено:
npm start

Также надо убедиться, что protractor установлен:
npm install

И что webdriver обновлен:
npm run update-webdriver

Попробуем теперь протестировать, что значение переменной query появляется в заголовке окна. Для этого изменим файл test/e2e/scenarios.js следующим образом:

describe('PhoneCat App', function() {

  describe('Phone list view', function() {

    beforeEach(function() {
      browser.get('app/index.html');
    });

    var phoneList = element.all(by.repeater('phone in phones'));
    var query = element(by.model('query'));

    it('should filter the phone list as a user types into the search box', function() {
      expect(phoneList.count()).toBe(3);

      query.sendKeys('nexus');
      expect(phoneList.count()).toBe(1);

      query.clear();
      query.sendKeys('motorola');
      expect(phoneList.count()).toBe(2);
    });

    it('should display the current filter value in the title bar', function() {
      query.clear();
      expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/);

      query.sendKeys('nexus');
      expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/);
    });
  });
});

Запустим  protractor:
npm run protractor