Роутинг и вьюхи в AngularJS

В этом уроке мы рассмотрим как создать шаблон макета (layout) и как создать приложение с несколькими представлениями с помощью роутинга используюя модуль 'ngRoute'.
  • Когда пользователь будет переходить на страницу app/index.html он будет перенаправляться на app/index.html/#/phones и список телефонов будет появляться в браузере.
  • Когда пользователь будет кликать на ссылку на телефон, url поменяется на соответствующий странице с деталями о телефоне.

Переключимся на данный шаг:


git checkout -f step-7

Список изменений, которые будут внесены на этом шаге.



Зависимости

Функциональность роутинга обеспечивается модулем ngRoute, который распространяется отдельно от core Angular фреймворка.

Мы используем Bower для установки зависимостей на клиентской стороне. Для того чтобы подключить новую зависимость надо обновить конфигурационный файл bower.json:
{
  "name": "angular-phonecat",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-phonecat",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.4.x",
    "angular-mocks": "1.4.x",
    "jquery": "~2.1.1",
    "bootstrap": "~3.1.1",
    "angular-route": "1.4.x"
  }
}

Новая зависимость "angular-route": "1.4.x" предписывает bower-у установить версию angular-route  компонента совместимую с версией 1.4.x.

Если bower установлен глобально, то можно выполнить bower install, но т.к. у нас преднастроен npm то он сделает это за нас:
npm install

Multiple Views, Routing и Layout Template

До этого шага у нас было всего одно представление (список телефонов), которые содержалось в файле index.html. Теперь нам необходимо представление отображающее детальную информацию об устройстве. Можно было бы также добавить это представление в файл index.html, но очень скоро мы бы запутались в коде. Поэтому мы преобразуем index.html в то что называется "layout template". Это главный шаблон для всех остальных представлений в нашем приложении. Другие "partial templates" будут включаться в этот главный шаблон, в зависимости от текущего "маршрута" ("route").

Маршруты в Angular-приложении объявляются через $routeProvider, который обеспечивается сервисом $route. Этот сервис позволяет легко связывать контроллеры, шаблоны представлений, и текущий URL в браузере. Используя эти возможности мы можем реализовать глубокое связывание, которое позволяет нам использовать историю браузера и закладки.

DI, Injector и Providers

Как было рассказано на прошлом шагеdependency injection (DI) является ядром AngularJS.

Когда приложение загружается, Angular создает инжектор, который будет использоваться для поиска и внедрения всех нужных сервисов. Сам инжектор ничего не знает о $http или $route сервисах. В действительности инжектор не знает даже о существовании этих сервисов, пока не будет сконфигурирован с соответствующими определениями модулей.

Инжектор выполняет следующие шаги:
  • загружает определения модулей, которые указаны в приложении;
  • регистрирует всех Провайдеров определенных в определениях модулей;
  • по запросу, внедряет указанную функцию и все необходимые зависимости (сервисы), которые лениво (lazily) инстанцируются через их Провайдеров.

Провайдеры - это объекты, который предоставляют (создают) экземпляры сервисов и предоставляют конфигурационный API, который можно использовать для управления созданием и поведением сервиса во время выполнения приложения. В случае с сервисом $route$routeProvider дает API, который позволяет определять маршруты в приложении.

Провайдеры могут быть внедрены только в config функции. Поэтому вы не можете внедрить $routeProvider в PhoneListCtrl.

Angular modules solve the problem of removing global state from the application and provide a way of configuring the injector. В отличие от AMD или require.js модулей, Angular модули не решают проблему порядка загрузки скриптов или ленивого получения скриптов. Эти цели полностью независимы и обе системы модулей могут сосуществовать решая свои проблемы


Understanding Dependency Injection

Шаблон

Сервис $route обычно используется в связке с директивой ngView. Роль директивы ngView подключить шаблон представления в layout template для текущего маршрута. Это то что нам нужно для шаблона index.html.

<!doctype html>
<html lang="en" ng-app="phonecatApp">
<head>
...
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/controllers.js"></script>
</head>
<body>

  <div ng-view></div>

</body>
</html>

Мы добавили два тега <script> для загрузки JavaScript-файлов:

  • angular-route.js: определяет модуль Angular ngRoute, который предоставляет роутинг.
  • app.js: этот файл содержит корневой модуль приложения.


Заметьте, что мы удалили весь код представления из файла index.html и заменили его единственной строкой содержащей элемент div с директивой ng-view. Код, который мы удалили переехал в файл phone-list.html:

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

      Search: <input ng-model="query">
      Sort by:
      <select ng-model="orderProp">
        <option value="name">Alphabetical</option>
        <option value="age">Newest</option>
      </select>

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

      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>

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

Мы также добавили шаблон placeholder-а для представления деталей телефона:
TBD: detail view for <span>{{phoneId}}</span>

Обратите внимание как мы используем phoneId выражение, которое определено в контроллере PhoneDetailCtrl.

App Module

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

Мы добавили angular-route.js в index.html и создали новый модуль phonecatControllers в controllers.js. Надо добавить эти модуль как зависимости для приложения, перечислив их как зависимости phonecatApp.
var phonecatApp = angular.module('phonecatApp', [
  'ngRoute',
  'phonecatControllers'
]);

...

Обратите внимание на второй аргумент angular.module['ngRoute', 'phonecatControllers'], этот массив содержит список модулей от которых зависит phonecatApp.

...

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl'
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl'
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

Используя метод phonecatApp.config(), мы внедряем $routeProvider в нашу конфигурационную функцию $routeProvider.when() для определения маршрутов.

Маршруты нашего приложения определяются следующим образом:

  • when('/phones'): Представление списка телефонов отображается, когда URL /phones. Для того чтобы построить это представление, Angular использует шаблон phone-list.html и контроллер PhoneListCtrl.
  • when('/phones/:phoneId'): Представление деталей телефона отображается когда URL соответствует '/phones/:phoneId', где :phoneId переменная часть URL. Для того чтобы построить это представление, Angular использует шаблон phone-detail.html и контроллер PhoneDetailCtrl.
  • otherwise({redirectTo: '/phones'}): вызывает перенаправление на /phones, когда адрес в браузере не соответствует ни одному из маршрутов.


Мы повторно использовали контроллер PhoneListCtrl, который мы сделали на предыдущих шагах, и мы добавили новый пустой контроллер PhoneDetailCtrl в файл app/js/controllers.js для представления деталей устройства.

PhoneListCtrl теперь работает с partials/phone-list.html, это означает например, что если вставить выражение {{orderProp}} в index.html или phone-detail.html, то оно не сработает. Это свойство видно только в области видимости подчиненной контроллеру PhoneListCtrl. Поэтому данный биндинг будет работать только если его вставить в файл phone-list.html.

Обратите внимание, что мы используем параметр :phoneId во втором определении маршрута. Сервис $route использует объявление маршрута - '/phones/:phoneId' - как шаблон, который соответствует текущему URL. Все переменные начинающиеся с : будут извлечены в объект $routeParams.

Контроллеры

var phonecatControllers = angular.module('phonecatControllers', []);

phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
  function ($scope, $http) {
    $http.get('phones/phones.json').success(function(data) {
      $scope.phones = data;
    });

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

phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
  function($scope, $routeParams) {
    $scope.phoneId = $routeParams.phoneId;
  }]);

Мы создали новый модуль phonecatControllers. Для маленьких AngularJS приложений в основном создает один модуль для всех контроллеров. По мере того как приложение растет в него могут добавляться новые модули. В больших приложениях на каждую отдельную большую возможность создается модуль.

Так как наше приложение маленькое, то мы все контроллеры добавили в модуль phonecatControllers.

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

Для того чтобы проверить, что всё связано правильно, мы напишем end-to-end тесты для различных URL и проверим, что они отображаются правильно.
...
   it('should redirect index.html to index.html#/phones', function() {
    browser.get('app/index.html');
    browser.getLocationAbsUrl().then(function(url) {
        expect(url).toEqual('/phones');
      });
  });

  describe('Phone list view', function() {
    beforeEach(function() {
      browser.get('app/index.html#/phones');
    });
...

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

    beforeEach(function() {
      browser.get('app/index.html#/phones/nexus-s');
    });


    it('should display placeholder page with phoneId', function() {
      expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
    });
  });

Теперь можно запустить тесты: npm run protractor