Учебник AngularJS: Всеобъемлющее руководство, часть 2

https://www.airpair.com/angularjs/posts/angularjs-tutorial
  • Перевод
  • Tutorial
Часть 1

4.1 $rootScope


$rootScope не сильно отличается от $scope, просто это объект $scope самого верхнего уровня, от которого происходят все остальные области видимости. Когда Angular начинает создание вашего приложение, он создаёт объект $rootScope, и все привязки и логика приложения создают объекты $scope, являющиеся наследниками $rootScope.

Обычно мы не используем $rootScope, но с его помощью можно обеспечить передачу данных между разными областями видимости.

5 Контроллеры


Контроллер позволяет взаимодействовать Виду и Модели. Это то место, где логика презентации синхронизирует интерфейс с моделью. Цель Контроллера – приводить в действие изменения в Модели и Виде. В Контроллере Angular сводит вместе бизнес-логику и логику презентации.

Мы уже касались Контроллеров, объявив атрибут ng-controller для показа данных $scope. Этот атрибут связывает область видимости и экземпляр Контроллера, и обеспечивает доступ к данным и методам Контроллера из DOM.

Перед использованием Контроллера его надо создать. Воспользуемся уже пройденным материалом:

angular
  .module('app', [])
  .controller('MainCtrl', function () {
});


Контроллер принимает два аргумента – имя, по которому на него можно ссылаться, и функцию обратного вызова. При этом на самом деле это будет функция, описывающая тело Контроллера.

Чтобы методы Angular не выглядели, как обратные вызовы, я размещаю функции вне синтаксиса Angular.

function MainCtrl () {

}

angular
  .module('app', [])
  .controller('MainCtrl', MainCtrl);


Так оно выглядит почище и читается легче. Я назвал функцию MainCtrl, потому что именованные функции легче отлаживать.

Последующие примеры подразумевают, что модуль был создан.

5.1 Методы и логика презентаций

Цель Контроллера – интерпретировать бизнес-логику модели и преобразовывать её в формат презентации. В Angular это можно делать по-разному, в зависимости от того, какие мы получаем данные.

Контроллер общается с Сервисом, и передаёт данные в том же, или изменённом формате в наш Вид через объект $scope. Когда Вид обновлён, логика Контроллера также обновляется, и её можно передавать обратно на сервер через Сервис. Но сначала мы создадим несколько объектов в Контроллере и свяжем их со $scope, чтобы разобраться, как работает Контроллер. Бизнес-логику мы расписывать не будем, для краткости мы добавим её ниже и получим данные от Сервиса.

function MainCtrl ($scope) {
  $scope.items = [{
    name: 'Набор ныряльщика',
    id: 7297510
  },{
    name: 'Шноркель',
    id: 0278916
  },{
    name: 'Гидрокостюм',
    id: 2389017
  },{
    name: 'Полотенце',
    id: 1000983
  }];
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


При помощи $scope мы привязываем массив. После этого он будет доступен в DOM, где его можно передать одной из встроенных Директив Angular, ng-repeat, чтобы в цикле пройтись по данным и создать структуру в DOM, основанную на шаблоне и данных.

<div ng-controller="MainCtrl">
  <ul>
      <li ng-repeat="item in items">
          {{ item.name }}
      </li>
  </ul>
</div>


5.2 Новый синтакс «controllerAs»

Контроллеры похожи на классы, но использование их через объекты $scope не похоже на использование классов. Они предлагали использовать ключевое слово this вместо $scope. Разработчики Angular ввели такую возможность в рамках синтакса controllerAs, где Контроллер оформляется в виде экземпляра, хранящегося в переменной – примерно так же, как использование new с переменной для создания нового объекта.

Мы опускаем вызов $scope и используем this.

function MainCtrl () {
  this.items = [{
    name: 'Набор ныряльщика',
    id: 7297510
  },{
    name: 'Шноркель',
    id: 0278916
  },{
    name: 'Гидрокостюм',
    id: 2389017
  },{
    name: 'Полотенце',
    id: 1000983
  }];
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


Потом мы добавляем “as” там, где нам нужно создать экземпляр Контроллера в DOM.

Запись MainCtrl as main означает, что все данные находятся в переменной main, поэтому items из прошлого примера превращаются в main.items.

<div ng-controller="MainCtrl as main">
  <ul>
      <li ng-repeat="item in main.items">
          {{ item.name }}
      </li>
  </ul>
</div>


Так понятнее, к какому Контроллеру принадлежит свойство, когда у нас есть много Контроллеров или вложенные Контроллеры. К тому же, это помогает избежать конфликт имён.

У каждого создаваемого $scope есть объект $parent. Без использования controllerAs нам надо бы было использовать ссылки на $parent для любых методов из области видимости $parent, и $parent.$parent для области на уровень выше, и так далее. Теперь же мы можем просто использовать имя переменной.

6 Сервисы и Фабрики


Сервисы позволяют хранить данные Модели и бизнес-логику, например, общение с сервером через HTTP. Часто путают Сервисы и Фабрики – отличие их в том, как создаются соответствующие объекты.

Важно помнить, что все Сервисы – синглтоны, на каждую инъекцию есть только один Сервис. По соглашению имена Сервисам даются в стиле паскаля, то есть «my service» пишется как «MyService».
6.1 Метод service

Метод создаёт новый объект, который общается с бэкендом и предоставляет инструменты для работы с бизнес-логикой. Сервис – это объект constructor, который вызывают через ключевое слово new, в связи с чем наша логика связывается с сервисом при помощи ключевого слова this. Сервис создаёт объект-синглтон.

function UserService () {
  this.sayHello = function (name) {
    return 'Привет тебе ' + name;
  };
}

angular
  .module('app')
  .service('UserService', UserService);


Теперь можно вставлять Сервис в Контроллер.

function MainCtrl (UserService) {
  this.sayHello = function (name) {
    UserService.sayHello(name);
  };
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


Перед Сервисом нельзя выполнять код, поскольку все методы создаются в виде объектов. У Фабрики всё обстоит иначе.

6.2 Фабричные методы

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

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

function UserService () {
  var UserService = {};
  function greeting (name) {
    return 'Привет тебе ' + name;
  }
  UserService.sayHello = function (name) {
    return greeting(name);
  };
  return UserService;
}

angular
  .module('app')
  .factory('UserService', UserService);


При помощи замыканий можно эмулировать приватную область видимости. Можно было бы создать что-то подобное и внутри метода service constructor, но здесь видно, что возвращается, а что остаётся внутри области видимости Сервиса. Так же можно создать приватные вспомогательные функции, которые остаются в области видимости, когда функция уже вернулась, при этом их можно использовать из публичных методов. Внутри Контроллера используются такие Сервисы точно так же.

function MainCtrl (UserService) {
  this.sayHello = function (name) {
    UserService.sayHello(name);
  };
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


Сервисы обычно используют не для логики презентаций, а для слоя бизнес-логики. Это общение с бэкендом через REST по Ajax.

7 Использование шаблонов через ядро Angular


Пока мы рассматривали программисткую сторону Angular, но не показывали, как же его можно использовать из HTML. Следующий шаг – использовать мощные возможности шаблонизации.

7.1 Выражения

Выражения Angular – это похожие на JavaScript сниппеты, которые можно использовать в шаблонах, чтобы проводить условные изменения в DOM – как в элементах, так и их свойствах, а также в тексте. Они живут внутри ссылок {{}} и выполняются в пределах $scope. Там нет циклов или if/else, и мы не можем выбрасывать исключения. Можно делать только мелкие операции или вызывать значения свойств $scope.

К примеру, {{ value }} – это выражение. В выражениях можно использовать логические операторы вроде || и &&, или тернарный оператор value? true: false.

С их помощью можно создавать гибкие шаблоны и обновлять переменные без перезагрузки страниц.

function MainCtrl () {
  this.items = [{
    name: 'Набор ныряльщика',
    id: 7297510
  },{
    name: 'Шноркель',
    id: 0278916
  },{
    name: 'Гидрокостюм',
    id: 2389017
  },{
    name: 'Полотенце',
    id: 1000983
  }];
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


Можно получить длину массива, объявив выражение, использующее свойство length.

<div ng-controller="MainCtrl as main">
  {{ main.items.length }} шт. в наличии
</div>


Angular заменит length на значение, и мы увидим “4 шт. в наличии”

7.2 Использование основных директив

После такого использования значения автоматически связываются внутри Angular, поэтому любые обновления влияют на значение выражений. Если мы уберём из массива один предмет, внешний вид автоматически обновится до “3 шт. в наличии”. Никакой возни, никаких лишних обратных вызовов.

Есть целая куча директив с префиксом ng-*. Начнём с ng-click и привяжем функцию к новому кусочку HTML-темплейта. В примере я прохожу по массиву и показываю количество товара в наличии.

<div ng-controller="MainCtrl as main">
  <div>
    {{ main.items.length }} шт. в наличии
  </div>
  <ul>
      <li ng-repeat="item in main.items" ng-click="main.removeFromStock(item, $index)">
          {{ item.name }}
      </li>
  </ul>
</div>


Атрибут ng-click связывается с функцией main.removeFromStock(). Я передаю ей товар, который в данный момент мы обрабатываем в цикле. Свойство $index полезно для удаления элементов из массива – нам не нужно вручную подсчитывать индекс текущего элемента.

Теперь функцию можно добавить к Контроллеру. В функцию передаются $index и элемент массива, и метод делает следующее:

function MainCtrl () {
  this.removeFromStock = function (item, index) {
    this.items.splice(index, 1);
  };
  this.items = [...];
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


При создании методов надо учитывать, что значение this может разниться, в зависимости от использования и контекста выполнения. Обычно я создаю ссылку на Контроллер как

var vm = this;

где vm – это ViewModel. При таком подходе ссылки не потеряются. Переработанный контроллер выглядит так:

function MainCtrl () {
  var vm = this;
  vm.removeFromStock = function (item, index) {
    vm.items.splice(index, 1);
  };
  vm.items = [...];
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);


Так и создаётся логика презентации, работающая с интерфейсом. Пропущен лишь один шаг – обновление Модели. Перед удалением элемента из vm.items мы должны отправить запрос DELETE на бэкенд, и после успешного ответа удалить его из массива. Тогда DOM обновится только в случае успешной обработки запроса и не введёт пользователя в заблуждение.

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

8 Директивы


Есть два вида Директив – одни работают со связками внутри Angular, а другие мы создаём сами. Директива может делать, что угодно – предоставлять логику для заданного элемента, или сама быть элементом и предоставлять шаблон с логикой внутри себя. Идея Директив в расширении возможностей HTML.

Посмотрим на встроенные директивы Angular, затем попробуем создать свои.

8.1 ng-repeat

Мы уже встречались с ней, поэтому я дам только простой пример.

<ul>
  <li ng-repeat="item in main.items">
    {{ item }}
  </li>
</ul>


ng-repeat копирует элемент и воспроизводит его, заполняя при этом данными объектов из массива. Если из массива удалить элемент, DOM обновится автоматически.

8.2 ng-model

Используется для инициализации новой несуществующей модели, или для привязки существующей.

Сообщение: {{ main.message }}



Если в свойстве $scope main.message содержится значение, оно будет передано в input. Если в $scope нет такого значения, оно просто будет проинициализировано. Мы можем передавать эти значения в другие директивы, например в ng-click.

8.3 ng-click

Прелесть его в том, что нам не надо самим навешивать обработчики событий. Angular сам подсчитает значение выражения внутри директивы и повесит обработчик на событие. В отличие от onClick=’’, директива ng-click принадлежит к своей области видимости, т.е. не является глобальной.

<input type="text" ng-model="main.message">
<a href=" ng-click="main.showMessage(main.message);">Показать сообщение</a>


Здесь я передаю main.message в метод main.showMessage, а там Angular обрабатывает его как простой объект JavaScript. В этом состоит прелесть Angular – все связки данных в DOM являются объектами, мы можем просто парсить их, манипулировать ими, преобразовывать в JSON и отправлять на бэкенд.

8.4 ng-href/ng-src

Чтобы Angular сам заботился об особенностях работы браузеров с параметрами href и src, вместо них мы используем ng-href=" and ng-src=".

<a ng-href="{{ main.someValue }}">Go</a>
<img ng-src="{{ main.anotherValue}}" alt=">


8.5 ng-class

Эта Директива выглядит как описание свойств и значений объекта. Вместо традиционных вызовов elem.addClass(className) и elem.removeClass(className), Angular добавляет и удаляет классы на основании заданных выражений.

<div class="notification" ng-class="{
  warning: main.response == 'error',
  ok: main.response == 'success'
}">
  {{ main.responseMsg }}
</div>


Angular выясняет значение main.response и в зависимости от него добавляет класс warning или ok.

8.6 ng-show/ng-hide

Эти Директивы часто встречаются при использовании Angular. Это удобный способ показывать и прятать данные в зависимости от значения свойства.

Для переключения видимости элемента мы используем ng-click.

<a href=" ng-click="showMenu = !showMenu">Переключить меню!</a>
<ul ng-show="showMenu">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>


Разница только в том, что ng-show или ng-hide определяют, должен ли элемент изначально быть видимым или быть спрятанным.

8.7 ng-if

ng-if не просто прячет элементы, а удаляет их. Если элемент нужно воссоздать после изменения значения свойства, для него создаётся новый $scope. Это также положительно влияет на быстродействие фреймворка.

<div ng-if="main.userExists">
  Введите логин
</div>


8.8 ng-switch

Директива, сходная с оператором case / switch в программировании, или продвинутый ng-if. В зависимости от значения из $scope выбирается один из нескольких элементов.

<div ng-switch on="main.user.access">
  <div ng-switch-when="admin">
    <!-- code for admins -->
  </div>
  <div ng-switch-when="user">
    <!-- code for users -->
  </div>
  <div ng-switch-when="author">
    <!-- code for authors -->
  </div>
</div>


8.9 ng-bind

Значения можно вставлять в DOM через синтаксис {{ value }}, но есть ещё один вариант, ng-bind. Различия синтаксиса видно на примере.

<p>{{ main.name }}</p>
<p ng-bind="main.name"></p>


ng-bind можно использовать, чтобы избежать мигания при загрузке страницы. Angular автоматически скрывает контент, который подгружается через Директивы. При использовании фигурных скобок они могут быть видны в тексте документа. При использовании ng-bind вставки не видны, пока Angular не вычислит нужные значения.

8.10 ng-view

Одностраничные приложения используют одну страницу без перезагрузок, содержимое которой обновляется автоматически. Это достигается использованием атрибута ng-view с пустым элементом вроде , который служит контейнером для любого динамически вставляемого кода, получаемого через XMLHttpRequest.

На место ng-view вставляются различные Виды, в зависимости от пути в URL. Например, можно сказать Angular, чтобы он вставлял login.html, если URL содержит myapp.com/#/login, и менять содержимое при изменении URL.

8.11 Расширение HTML

Хотя встроенные Директивы добавляют функциональности в HTML, иногда необходимо добавить свои функции для дальнейшего расширения возможностей. Рассмотрим API для создания своих директив.

9 Настраиваемые Директивы

Настраиваемые Директивы – одна из самых сложных концепций в API Angular, потому что они не похожи на привычные программные концепции. Они выступают в роли способа Angular реализовать концепции Веб ближайшего будущего – настраиваемые элементы, теневой DOM, шаблоны и импорт HTML. Давайте постепенно изучим Директивы, разбив их на слои.

9.1 Настраиваемые элементы

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

В Angular есть четыре способа использования Директив – Настраиваемые элементы, Настраиваемые атрибуты, имена классов и комментарии. Последних двух я стараюсь избегать, потому что в них легко запутаться, и к тому же, у комментариев есть проблемы с IE. Самый безопасный и кроссбраузерный способ – Настраиваемые атрибуты. Давайте рассмотрим эти способы в следующем порядке: Element, Attribute, Class, Comment.

<my-element></my-element>

<div my-element></div>

<div class="my-element"></div>

<!-- directive: my-element -->


У Директив есть свойство restrict, через которое можно ограничить их использование одним из этих способов. По умолчанию, Директивы используются через ‘EA’, что означает Element, Attribute. Другие варианты – C для классов и М для комментариев.

9.2 Теневой DOM

Теневой DOM работает так, что внутри определённых частей обычного DOM документа содержатся вложенные документы. Они поддерживают HTML, CSS и области видимости JavaScript.

В Теневом DOM можно определять как чистый HTML, так и контент, который будет в него импортирован. Мы можем поместить текст в Настраиваемый элемент:

<my-element>
  Всем привет!
</my-element>


И текст «Всем привет!» доступен в Теневом DOM.

9.3 Шаблонизация и импорт HTML

Существует три различных способа использования шаблонов в Директивах. Два почти одинаковых, а третий хранит шаблон в строке.

9.3.1 свойство template

Объявляет нужный шаблон. Шаблон форматируется как строка, а затем Angular компилирует его и вставляет в DOM

Пример:

{
  template: '<div>' + 
    '<ul>' +
      '<li ng-repeat="item in vm.items">' +
        '{{ item }}' +
      '</li>' +
    '</ul>' +
  '</div>'
}


При использовании такого подхода удобно вписывать логику на JavaScript между строками. Я использую конструкцию с [].join(''), она делает текст более читаемым.

{
  template: [
    '<div>',
      '<ul>',
        '<li ng-repeat="item in vm.items">',
          '{{ item }}',
        '</li>',
      '</ul>',
    '</div>'
  ].join('')
}


9.3.2 свойство templateUrl

Свойство templateUrl позволяет указывать на внешний ресурс или на элемент
 , которые содержат нужный шаблон.

Если задать:

{ templateUrl: 'items.html' }


Angular сначала поищет в DOM элемент
 с подходящим id, а если не найдёт, тогда запросит документ через HTTP GET.

<script type="text/ng-template" id="/hello.html"> <div> <ul> <li ng-repeat="item in vm.items"> {{ item }} </li> </ul> </div> </script>


Сначала мы задаём тип шаблона как text/ng-template, чтобы браузер не интерпретировал его текст, как JavaScript. Таким образом можно включать текст шаблонов в файл, а не скачивать его отдельными файлами. Также можно использовать свойство template, когда шаблон хранится в строке. Когда шаблон загружается, Angular сохраняет его, и потом его можно использовать в ng-include и ng-view. В ином случае Angular осуществит GET-запрос файла с шаблоном.

Все загруженные шаблоны хранятся в $templateCache в течение всей жизни приложения.

9.4 API Директив

Давайте поподробнее рассмотрим API Директив, чтобы можно было создавать свои директивы. Первая инструкция – return, возвращающая объект. Это всё, что нужно для создания директивы.

function someDirective () {
  return {

  };
}

angular
  .module('app')
  .directive('someDirective', someDirective);


Посмотрим на самые популярные свойства объектов директив.

function someDirective () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    controllerAs: 'something',
    controller: function () {

    },
    link: function ($scope, $element, $attrs) {

    },
    template: [
      '<div class="some-directive">',
        'My directive!',
      '</div>'
    ].join('')
  };
}

angular
  .module('app')
  .directive('someDirective', someDirective);


9.4.1 restrict

Мы уже упоминали restrict – она позволяет ограничивать использование директивы. Если мы хотим задавать директиву только через атрибуты, можно ограничить их ‘A’. Для ограничения работы директив в качестве элементов используется ‘E’, комментариев – ‘M’ и имён классов – ‘C’.

9.4.2 replace

Заменяет оригинальный элемент директивы. Если мы используем <some-directive></some-directive> и задаём replace: true, после создания страницы изначальный элемент будет заменён результатом работы скрипта.

9.4.3 scope

Позволяет наследовать $scope текущего или родительского контекста, в который входит директива. Можно создать изолированный $scope и передавать определённые значения, обычно через настраиваемые атрибуты.

9.4.4 controllerAs

Мы уже пробовали это свойство, которое определяет имя контроллера внутри директивы. Если мы задаём controllerAs: 'something', то все ссылки на свойства контроллера будут выглядеть как something.myMethod()

9.4.5 controller

Захватить существующий контроллер или создать новый. Если MainCtrl уже существует, можно определить его как controller: 'MainCtrl'. Для сохранения инкапсуляции мы просто объявляем новый контроллер каждый раз через controller: function () {}. Функция обратного вызова контроллера должна обрабатывать изменения в ViewModel и общаться с Сервисами.

9.4.6 link

Функция link вызывается после того, как элемент компилируется и вставляется в DOM, поэтому здесь можно сделать что-то с контентом после компиляции или что-то, не связанное с Angular.

В контроллере мы не манипулируем DOM, но это возможно в функции link. Она также может вставлять $scope, корневой элемент шаблона $element и объект $attrs, содержащий все свойства элемента DOM, отражающего текущее состояние {{ }}. Внутри link можно привязать обработчики событий, определить плагины и даже вставить сервисы Angular.

9.5 Создание директив

Пройдёмся по примеру создания директивы, которая позволяет вставлять компонент “email”, у которого есть поля To, Subject и Message.

Мы создаём элемент <compose-email>, на место которого происходит вставка контента. Его можно вставить в разные места DOM многократно, и везде он будет заменён на наш компонент, каждый из которых будет отдельным экземпляром. Начнём с шаблонов.

function composeEmail () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    controllerAs: 'compose',
    controller: function () {

    },
    link: function ($scope, $element, $attrs) {

    },
    template: [
      '<div class="compose-email">',
        '<input type="text" placeholder="To..." ng-model="compose.to">',
        '<input type="text" placeholder="Subject..." ng-model="compose.subject">',
        '<textarea placeholder="Message..." ng-model="compose.message"></textarea>',
      '</div>'
    ].join('')
  };
}

angular
  .module('app')
  .controller('composeEmail', composeEmail);


Теперь мы можем использовать директиву composeEmail в нескольких местах, без необходимости копировать куски HTML. Помните, что Angular парсит название директивы так, что composeEmail превращается в <compose-email></compose-email>

10 Фильтры


Фильтры обрабатывают информацию и выдают определённые наборы данных, основываясь на какой-либо логике. Это может быть что угодно, от форматирования даты в читаемый формат, до списка имён, которые начинаются на заданную букву. Посмотрим на популярные встроенные фильтры.

Их можно использовать либо в DOM через символ вертикальной черты | в выражениях, которые парсит Angular, либо через сервис $filter, который можно использовать в JavaScript коде вместо HTML.

HTML синтакс:

{{ filter_expression | filter : expression : comparator }}


JavaScript синтакс:

$filter('filter')(array, expression, comparator);


Чаще используется более простой вариант с HTML. Вот некоторые из фильтров.

10.1 Фильтры даты

С датами работать муторно, но Angular облегчает эту задачу. Привяжем фильтр к дате в миллисекундах к $scope.timeNow = new Date().getTime():

<p>
  Сегодня у нас: {{ timeNow | date:'dd-MM-yyyy' }}
</p>


10.2 Фильтр JSON

Встроенные фильтры JSON преобразуют объект JavaScript в строку JSON. Это удобно для вывода значений Модели в DOM при разработке. Для красивого форматирования оберните фильтры JSON в тэги
</code>

<source lang="html">
<pre>
{{ myObject | json }}



10.3 limitTo и orderBy

Описанные фильтры работают просто – получают значение и выдают значение. Как же работать с наборами данных?

limitTo ограничивает количество данных, передаваемых в Вид – его удобно использовать внутри ng-repeat.

<ul>
  <li ng-repeat="user in users | limitTo:10">
    {{ user.name }}
  </li>
</ul>


Фрагмент выведет не более 10 пользователей. Обратите внимание на использование limitTo внутри ng-repeat.

orderBy задаёт порядок вывода массива, сортируя по одному из свойств объектов. Если у нас объект пользователя выглядит так:

{
  name: 'Todd Motto',
}


мы можем вывести лист по алфавиту следующим образом:

<ul>
  <li ng-repeat=" user in users | orderBy:'name' ">
    {{ user.name }}
  </li>
</ul>


Настоящие возможности фильтров раскрываются при создании своих собственных.

11 Собственные фильтры


Мы все делали фильтры в объектах и массивах. Для использования их в Angular существует API. Мы имеем двустороннюю связь данных безо всякого труда. Все заданные нами фильтры будут вызваны в цикле $digest.

11.1 Фильтры одного значения

Принимают одно значение, и выдают отфильтрованный контент. Для создания используется метод .filter(). Все такие фильтры глобально доступны в любой области видимости.

Вот заготовка фильтра, от которой можно отталкиваться.

function myFilter () {
  return function () {
    // возврат результата
  };
}
angular
  .module('app')
  .filter('myFilter', myFilter);


Все аргументы для фильтров передаются внутрь автоматически. Для примера сделаем фильтр форматирования текста в нижний регистр (хотя у Angular есть такой встроенный фильтр).

function toLowercase () {
  return function (item) {
    return item.toLowerCase();
  };
}
angular
  .module('app')
  .filter('toLowercase', toLowercase);


item передаётся фильтру как локальная переменная. Используется фильтр точно так же, как встроенные:

<p>{{ user.name | toLowercase }}</p>


11.2 Фильтры наборов данных

Иногда надо обработать набор данных и вернуть их преобразованными. Давайте сделаем фильтр, который отбирает имена по заданной первой букве. Например, вот фильтр слов, начинающихся с 'A'.

function namesStartingWithA () {
  return function (items) {
    return items.filter(function (item) {
      return /$a/i.test(item.name);
    });
  };
}
angular
  .module('app')
  .filter('namesStartingWithA', namesStartingWithA);


Используем его внутри ng-repeat:

<ul>
  <li ng-repeat="item in items | namesStartingWithA">
    {{ item }}
  </li>
</ul>


Передавать аргументы в фильтры можно через двоеточие:

<ul>
  <li ng-repeat="item in items | namesStartingWithA:something">
    {{ item }}
  </li>
</ul>


something автоматически передаётся в функцию фильтра:

function namesStartingWithA () {
  return function (items, something) {
    // есть доступ и к "items", и к "something"
  };
}
angular
  .module('app')
  .filter('namesStartingWithA', namesStartingWithA);


11.3 Фильтры контроллеров


Фильтры можно создавать вне метода .filter(), просто передавая функцию в контроллер, которая будет выполнять роль фильтра. Продолжая предыдущий пример, мы можем создать функцию контроллера this.namesStartingWithA, тогда фильтр будет доступен только из этого контроллера, а не глобально. В этом случае используется синтакс controllerAs.

function SomeCtrl () {
  this.namesStartingWithA = function () {

  };
}
angular
  .module('app')
  .controller('SomeCtrl', SomeCtrl);


Синтакс вызова фильтра в DOM слегка отличается:

<ul>
  <li ng-repeat="item in vm.items | filter:namesStartingWithA">
    {{ item }}
  </li>
</ul>


12 Динамический роутинг через $routeProvider


Мы разбираемся с разными концепциями Angular, но пока не дошли до главной: как же Angular помогает создать одностраничное приложение. А для этого используется роутер, который мы настраиваем так, чтобы он менял состояние нашего приложения в зависимости от URL. Нужный нам роутер называется ngRoute

angular
  .module('app', [
    'ngRoute'
  ]);


Подключив таким образом нужный модуль, мы можем настраивать пути, вставляя $routeProvider и настраивая его в методе .config(). После этого мы получаем доступ к методу .when().

Представим, что у меня есть приложение для чтения почты – по нажатию на «входящие» /inbox нужно, чтобы оно показывало список писем. Тогда первый аргумент в .when() будет строкой, описывающей нужный URL. Второй аргумент – объект с дополнительными настройками. Есть ещё метод .otherwise(), который обрабатывает случаи несуществующих путей.

function router ($routeProvider) {
  $routeProvider
  .when('/inbox', {})
  .otherwise({
    redirectTo: '/inbox'
  });
}

angular
  .module('app')
  .config(router);


Для настройки пути сначала необходимо выбрать шаблон, который будет использоваться с нужным Видом. Предположим, мы используем шаблон inbox.html.

$routeProvider
.when('/inbox', {
  templateUrl: 'views/inbox.html'
})
.otherwise({
  redirectTo: '/inbox'
});


Всем Видам необходим Контроллер. У Angular для этого есть свойства controller и controllerAs.

$routeProvider
.when('/inbox', {
  templateUrl: 'views/inbox.html',
  controller: 'InboxCtrl',
  controllerAs: 'inbox'
})
.otherwise({
  redirectTo: '/inbox'
});


Теперь допустим, мы хотим настроить ещё один Вид – когда мы щёлкаем по входящему письму, мы должны увидеть само письмо. Для этого необходим динамический роутинг, поскольку у разных писем должны быть разные URL (содержащие id и т.п.). Для передачи данных динамического роутинга используется двоеточие перед названием динамической группы. Например, в случае письма с id 173921938 путь будет /inbox/email/173921938, а описание этого пути - '/inbox/email/:id'.

Когда приложение получает путь /inbox/email/173921938, Angular загружает те же самые шаблон и контроллер, что и для /inbox/email/902827312.

Итоговый пример выглядит так:

function router ($routeProvider) {
  $routeProvider
  .when('/inbox', {
    templateUrl: 'views/inbox.html',
    controller: 'InboxCtrl',
    controllerAs: 'inbox'
  })
   .when('/inbox/email/:id', {
    templateUrl: 'views/email.html',
    controller: 'EmailCtrl',
    controllerAs: 'email'
  })
  .otherwise({
    redirectTo: '/inbox'
  });
});

angular
  .module('app')
  .config(router);


Это основа для приложения, использующего пути. Но нужно ещё указать, куда на странице необходимо вставлять обработанный шаблон. Для этого используется ng-view. Обычно достаточно написать что-то вроде:

<div ng-view></div>


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

12.1 $routeParams

Сервис $routeParams автоматически парсит URL и выделяет из него набор параметров, который преобразовывает в объект. Например, в последнем примере будет извлечён id письма на основании задания динамического роутинга через :id

Простой пример контроллера, передающего параметры из $routeParams:

function EmailCtrl ($routeParams, EmailService) {
  // $routeParams { id: 20999851 }
  EmailService
  .get($routeParams.id) // передать объект
  .success(function (response) {})
  .error(function (reason) {});
}
angular
  .module('app')
  .('EmailCtrl', EmailCtrl);


13 Проверка форм


Проверка форм осуществляет несколько действий, от проверки изменений Модели и сверки их с существующими правилами связки, до изменения DOM для обратной связи с пользователем.

Для создания такой формы необходимо задать ей имя, определяющее область видимости формы.

<form name="myForm"></form>


Angular распознает такую форму и будет проверять и отслеживать ввод пользователя, например, заполнил ли тот обязательные поля формы, и т.п.

13.1 HTML5

В HTML5 был добавлен атрибут pattern, который позволял браузеру проверять ввод согласно с заданной регуляркой. Angular включает подобные возможности. Кроме этого, Angular переработал проверку полей, помеченных как required в своей директиве под названием ng-required, которая тоже постоянно отслеживает состояние Модели. Давайте посмотрим ещё на несколько возможностей Angular.

13.2 $pristine

После первоначального создания страницы Angular добавляет к форме свойство $pristine – это означает, что пользователь к ней ещё не притрагивался. Angular добавит класс ng-pristine к тем элементам, которые ещё не менялись.

13.4 $dirty

Противоположность pristine – это элементы, которые пользователь изменил. К ним добавляются классы ng-dirty, а классы ng-pristine удаляются. Форма не может вернуться к состоянию $pristine без перезагрузки страницы.

13.5 $valid

Каждое поле ввода может быть объявлено $valid. Например, если у поля есть атрибут ng-required, а пользователь заполнил его, то ему присваивается класс ng-valid

13.6 $invalid

Противоположность valid. По умолчанию, все формы находятся в состоянии $invalid – им присвоен класс ng-invalid. Переходы между этими двумя состояниями происходят по мере того, как пользователь вводит информацию.

13.7 Проверка на основе модели

В некоторых случаях нужно отключать или включать поля ввода или кнопки – например, пока пользователь что-то не введёт. Простой пример переключения состояний кнопки, основанный на данных Модели. Если пользователь не ввёл имя, форму нельзя обновлять.

<input type="text" ng-model="user.name" placeholder="Введите имя">
<button ng-disabled="!user.name.length">
  Обновить имя
</button>


Если user.name.length возвращает true, кнопка включается. И это состояние постоянно отслеживается всё время, пока происходит работа с формой.

14 Общение с сервером через $http и $resource


Для связи с сервером в Angular есть два API, $http и $resource. Это высокоуровневые способы связи с сервером, которые также запускают циклы $digest, поддерживая наши данные в актуальном состоянии.

14.1 $http

Если вы пользовались методом jQuery $.ajax, то вам всё сразу будет понятно. Метод $http можно использовать как функцию и как объект. Мы можем передать ему объект с настройками $http({...}), или же использовать доступные методы вроде $http.get(...). Метод $http основан на API обещаний, которое предоставляется встроенным сервисом $q.

Мне больше нравятся методы с короткой записью, поэтому для демонстрации работы я буду использовать именно их. Вот простой HTTP-запрос GET:

$http.get('/url')
.success(function (data, status, headers, config) {

})
.error(function (data, status, headers, config) {

});


Успешные и неудавшиеся обратные вызовы происходят асинхронно, и в ответе содержатся данные, статус и заголовки. Обычно все сразу они нам не нужны, поэтому можно использовать простой подход:

$http.get('/url')
.success(function (response) {

})
.error(function (reason) {

});


Мне нравятся использовать шаблоны обещаний при помощи метода .then(), поддерживающегося в Angular:

$http.get('/url')
.then(function (response) {
  // успех
}, function (reason) {
  // провал
});


В отличие от библиотеки jQuery, Angular оборачивает вызовы $http в $scope.$apply(), что запускает цикл запроса данных с сервера и обновляет связи.

14.2 $resource

Вместе с $http можно использовать модуль ngResource. В нём есть API $resource, удобное для CRUD-операций (create, read, update, delete). $resource создаёт объект, через который можно общаться с источниками данных по протоколам REST.

function MovieService ($resource) {
  return $resource('/api/movies/:id', { id: '@_id' },
    {
      update: {
        method: 'PUT' 
      }
    }
  );
}
angular
  .module('app')
  .factory('MovieService', MovieService);


Можно вставить зависимость в простую фабрику Movies в наши контроллеры и получать таким образом все нужные данные.

function MovieCtrl (MovieService) {
  var movies = new MovieService();
  // произошло обновление фильма
  movies.update(/* some data */);
}
angular
  .module('app')
  .controller('MovieCtrl', MovieCtrl);
Поделиться публикацией

Похожие публикации

Комментарии 23
    +1
    Хочу еще раз поблагодарить автора за работу по переводу. Это нужно и важно для сообщества. Добра вам и удачи в Новом Году.
      +3
      Не знаю как для новичков, но мне для закрепления полученных на практике знаний, в самый раз. Спасибо!
        0
        Спасибо автору за перевод. Действительно, для закрепления знаний весьма полезно. Если вводная статья показалась пустой по содержанию, то, прочитав эту, буду следить за обновлениями.

        Кстати, замечал, что использование $rootScope для хранения глобально-доступных данных немного ускоряет работу, в отличае от привызки к объекту window.
          0
          Ну это потому что rootScope сама по себе уже внутри, а поскольку обычно говорят что хранить переменные в глобальной области window это «айайай» то вывод напрашивается сам собой.
          PS. Можно поднять планку производительности ещё выше используя сервис через .factory, поскольку при inject'e возвращаться будет один и тот же самый объект. Считается что хранить что-то или обмениваться данными между контроллерами лучше всего так
            0
            Благодарю за наводку. Получилось что-то вроде этого:

            module.factory('MySvc', function () {
                  var idx = [],
                  service = {
                      add: function (id) {
                          idx.push(id);
                      },     
                      get: function () {
                          return idx;
                      }
                  }
                  return service;
            });
            
              0
              service = {

              Я бы вам не советовал инициализировать глобальную переменную.
                0
                Она разве глобальная в данном контексте?

                var idx = [],
                    service
                
                  0
                  пардон, не заметил запятую :)
                0
                Просто интересно, зачем оборачивать массив сверху объектом было? Можно же просто массив вернуть и не морочиться. Хотя если там ещё функций 20 для работы с ним, то тогда вопрос снимается автоматом :)
                  0
                  Не 20, конечно, но они есть или же «могут быть», поэтому привык к такому написанию.)
            +2
            Вот это вообще гениальная вещь. завтра же поменяю синтаксис всех шаблонов на такой.

                template: [
                  '<div class="compose-email">',
                    '<input type="text" placeholder="To..." ng-model="compose.to">',
                    '<input type="text" placeholder="Subject..." ng-model="compose.subject">',
                    '<textarea placeholder="Message..." ng-model="compose.message"></textarea>',
                  '</div>'
                ].join('')
            
              0
              Форма не может вернуться к состоянию $pristine без перезагрузки страницы.

              Может :)
                0
                А почему
                    var movie = new MovieService();
                    movie.update(/*data*/);
                


                А не сразу
                    MovieService.update(/*data*/);
                

                ?
                  0
                  Потому что factory. Понял. Т.е. такой подход создает по одному объекту ресурсу на приложение. А если делать ресурсы через сервисы?
                    0
                    Тогда можно спокойно сразу писать как вы и хотели. Мне так даже кажется удобнее, особенно когда мне необходимо сделать extend сервиса для другой сущности (типично для crud приложений).
                      0
                      Я сейчас посмотрел. Я так и делал. Просто я пришел из Java в AngularJS. Мне пока непонятна разница между фактори и сервисом.
                        0
                          0
                          Ну если вобще вобще по секрету, то разницы нет никакой. Всё зависит от того что напихать в return.
                          Я тоже джавист а на js пишу для себя. Главное никогда не заблуждаться что в js есть классы и некое подобие ооп.
                    0
                    plnkr.co/edit/NKs7X5gVcZmlWN7uPBYN?p=preview А что тут не так <compose-email></compose-email>
                    Упростил шаблон — но все равно что -то не работает
                      0
                      plnkr.co/edit/pkqGZdkjChdyHb9ae2np максимально упростил и приблизил к примеру из статьи — все равно не работает
                        0
                        Разобрался…
                        .directive('composeEmail', composeEmail); 
                        
                        вот так надо. т.е. не controller, а .directive
                        Опечатка у Вас
                          0
                          Спасибо огромное. Вы очень помогли
                        0
                        Шаблоны Angular == Smarty для фронтенда.
                        С содроганием вспоминаю эпоху шаблонизаторов на бэкенде, теперь же те же грабли повторяем на фронтенде :(

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое