Двухсторонняя привязка данных в AngularJS

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



Переключим репу на соответсвующее состояние:


git checkout -f step-4

На этом шаге мы внесем такие изменения.

В файл app/index.html внесем следующие изменения:
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
  <option value="name">Alphabetical</option>
  <option value="age">Newest</option>
</select>


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

Мы добавили элемент <select> с названием orderProp для того чтобы пользователь мог выбрать тип сортировки.


Далее мы выстроили цепочку обработки входных данных для цикла. Сначала идет filter, а потом orderBy.

Angular создает двухстороннюю привязку данных между select-элементом и свойством в модели orderProp, которой потом используется в качестве значения для фильтра orderBy.

Как уже было замечено ранее всякий раз когда модель изменяется (например потому что пользователь изменил порядок сортировки в выпадающем меню) механизм привязки данных автоматически обновляет представление. И не нужно делать никаких манипуляций с DOM моделью страницы.

В файл app/js/controllers.js внесем следующие изменения:
var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function ($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.',
     'age': 1},
    {'name': 'Motorola XOOM™ with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.',
     'age': 2},
    {'name': 'MOTOROLA XOOM™',
     'snippet': 'The Next, Next Generation tablet.',
     'age': 3}
  ];

  $scope.orderProp = 'age';
});

Мы добавили свойство age, которое будем использовать для сортировки телефонов.

Мы установили значение age по умолчанию для свойства orderProp. Если этого не сделать то фильтр останется orderBy неинициализированным до тех пор пока пользователь не выберет один из элементов из выпадающего меню. Если удалить эту строчку то Angular временно добавит новый пустой ("unknown") элемент в выпадающее меню и сортировка будет соответствовать исходному порядку.


Чтобы сделать сортировку в обратном порядке надо добавить - перед значением сортировки:
<option value="-age">Oldest</option>

Аналогично предыдущему шагу мы можем вывести текстовое значение свойства {{orderProp}} на страницу.

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

Сделаем модульный тест для того чтобы протестировать внесенные изменения. Для этого изменим файл test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() {

  describe('PhoneListCtrl', function(){
    var scope, ctrl;

    beforeEach(module('phonecatApp'));

    beforeEach(inject(function($controller) {
      scope = {};
      ctrl = $controller('PhoneListCtrl', {$scope:scope});
    }));

    it('should create "phones" model with 3 phones', function() {
      expect(scope.phones.length).toBe(3);
    });


    it('should set the default value of orderProp model', function() {
      expect(scope.orderProp).toBe('age');
    });
  });
});

Тут используется Jasmine API для того чтобы вынести конструирование контроллера в beforeEach блок, который находится в общем использовании у всех тестов в родительском describe блоке.

Результат должен быть следующим:
Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)

Теперь перейдем к end-to-end тесту, изменим файл test/e2e/scenarios.js:
...
it('should be possible to control phone order via the drop down select box', function() {

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

  function getNames() {
    return phoneNameColumn.map(function(elm) {
      return elm.getText();
    });
  }

  query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter

  expect(getNames()).toEqual([
    "Motorola XOOM\u2122 with Wi-Fi",
    "MOTOROLA XOOM\u2122"
  ]);

  element(by.model('orderProp')).element(by.css('option[value="name"]')).click();

  expect(getNames()).toEqual([
    "MOTOROLA XOOM\u2122",
    "Motorola XOOM\u2122 with Wi-Fi"
  ]);
});...
Теперь можно запустить тест: npm run protractor