Содержание

1 Введение в AngularJS
2 Engineering concepts in JavaScript frameworks
3 Modules
4 Understanding $scope
5 Controllers
6 Services and Factories
7 Templating with the Angular core
8 Directives (Core)
9 Directives (Custom)
10 Filters (Core)
11 Filters (Custom)
12 Dynamic routing with $routeProvider
13 Form Validation
14 Server communication with $http and $resource

1 Введение в AngularJS

Angular – MVW-фреймворк для разработки качественных клиентских веб-приложений на JavaScript. Он создан и поддерживается в Google и предлагает взглянуть на будущее веба, на то, какие новые возможности и стандарты он готовит для нас.

MVW означает Model-View-Whatever (модель – вид – что угодно), то есть гибкость в выборе шаблонов проектирования при разработке приложений. Мы можем выбрать модели MVC (Model-View-Controller) или MVVM (Model-View-ViewModel).

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

AngularJS позиционирует себя как фреймворк, улучшающий HTML. Он собрал концепции из разных языков программирования, как JavaScript, так и серверных, и делает из HTML также нечто динамическое. Мы получаем подход, основанный на данных, к разработке приложений. Нет нужды обновлять Модель, DOM или делать какие-то другие затратные по времени операции, например, исправлять ошибки браузеров. Мы концентрируемся на данных, данные же заботятся об HTML, а мы просто занимаемся программированием приложения.

Инженерные концепции в фрейморках JavaScript

Позиция AngularJS по работе с данными и другими инженерными концепциями отличается от таких фреймворков, как Backbone.js and Ember.js. Мы довольствуемся уже известным нам HTML, а Angular самостоятельно его улучшает. Angular обновляет DOM при любых изменениях Модели, которая живёт себе в чистых Объектах JavaScript с целью связи с данными. Когда обновляется Модель, Angular обновляет Объекты, которые содержат актуальную информацию о состоянии приложения.

2.1 MVC и MVVM

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

Держитесь за кресло – в Angular существует разделение обязанностей и динамический HTML. А это значит, что наши данные живут в Модели, наш HTML живёт в виде маленького шаблона, который будет преобразован в Вид, а Контроллер мы используем для соединения двух этих понятий, обеспечивая поддержку изменений Модели и Вида. То есть, навигация может выводиться динамически, создаваясь из одного элемента списка, и автоматически повторяться для каждого пункта из Модели. Это упрощённая концепция, позже мы ещё поговорим о шаблонах.

Разница между MVC и MVVM в том, что MVVM специально предназначен для разработки интерфейсов. Вид состоит из слоя презентации, ВидМодель содержит логику презентации, а Модель содержит бизнес-логику и данные. MVVM была разработана для облегчения двусторонней связи данных, на чём и процветают фреймворки типа AngularJS. Мы сосредоточимся на пути MVVM, так как в последние годы Angular склоняется именно туда.

2.2 Двусторонняя связь данных

Двусторонняя связь данных – очень простая концепция, предоставляющая синхронизацию между слоями Модели и Вида. Изменения Модели передаются в Вид, а изменения Вида автоматически отражаются в Модели. Таким образом, Модель становится актуальным источником данных о состоянии приложения.

Angular использует простые Объекты JavaScript для синхронизации Модели и Вида, в результате чего обновлять любой из них легко и приятно. Angular преобразовывает данные в JSON и лучше всего общается методом REST. При помощи такого подхода проще строить фронтенд-приложения, потому что всё состояние приложения хранится в браузере, а не передаётся с сервера по кусочкам, и нет опасения, что состояние будет испорчено или потеряно.

Связываем мы эти значения через выражения Angular, которые доступны в виде управляющих шаблонов. Также мы можем связывать Модели через атрибут под названием ng-model. Angular использует свои атрибуты для разных API, которые обращаются к ядру Angular.

2.3 Инъекция зависимостей (Dependency Injection, DI)

DI – шаблон разработки программ, который определяет, как компоненты связываются со своими зависимостями. Инъекция — это передача зависимости к зависимому Объекту, и эти зависимости часто называют Сервисами.

В AngularJS мы хитрым образом используем аргументы функции для объявления нужных зависимостей, а Angular передаёт их нам. Если мы забудем передать зависимость, но сошлёмся на неё там, где она нужна нам, Сервис будет не определен и в результате произойдёт ошибка компиляции внутри Angular. Но не волнуйтесь, angular выбрасывает свои ошибки и они очень просты в отладке.

2.4 Приложения на одну страницу (Single Page Application, SPA), управление состоянием и Ajax (HTTP)

В приложении на одну страницу (SPA) либо весь необходимый код (HTML, CSS and JavaScript) вызывается за одну загрузку страницы, либо нужные ресурсы подключаются динамически и добавляются к странице по необходимости, обычно в ответ на действия пользователя. Страница не перезагружается во время работы, не передаёт управление другой странице, хотя современные технологии из HTML5 позволяют одному приложению работать на нескольких логических страницах. Взаимодействие с SPA часто происходит при помощи фонового общения с сервером.

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

Времена изменились, браузер хранит состояние приложение, сложная логика и фреймворки приобрели популярность. AngularJS хранит состояние в браузере и передаёт изменения при необходимости через Ajax (HTTP) с использованием методом GET, POST, PUT и DELETE. Красота в том, что сервер может быть независим от фротенда, а фронтенд – от сервера. Те же самые сервера могут работать с мобильными приложениями с совершенно другим фронтендом. Это даёт нам гибкость, так как на бэкенде мы работаем с JSON-данными любым удобным нам способом на любом серверном ЯП.

2.5 Структура приложения

У Angular есть разные API, но структура приложения обычно одна и та же, поэтому почти все приложения строятся сходным образом и разработчики могут включаться в проект без усилий. Также это даёт предсказуемые API и процессы отладки, что уменьшает время разработки и быстрое прототипирование. Angular построен вокруг возможности тестирования («testability»), чтобы быть наиболее простым как в разработке, так и в тестировании.

Давайте изучать.

3 Модули

Все приложения создаются через модули. Модуль может зависеть от других, или быть одиночным. Модули служат контейнерами для разных разделов приложения, таким образом делая код пригодным для повторного использования. Для создания модуля применяется глобальный Object, пространство имён фреймворка, и метод module.

3.1 Сеттеры (setters).

У приложения есть один модуль app.

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

Вторым аргументом идёт [] – обычно этот массив содержит зависимости модуля, которые нам нужно подключить. Модули могут зависеть от других модулей, которые в свою очередь тоже могут иметь зависимости. В нашем случае массив пустой.

3.2 Геттеры (Getters)

Для создания Controllers, Directives, Services и других возможностей нам надо сослаться на существующий модуль. В синтаксисе есть незаметное различие – мы не используем второй аргумент.

angular.module('app');

3.3 Работа модулей

Модули могут храниться и вызываться и через переменную. Вот пример хранения модуля в переменной.

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

Теперь мы можем использовать переменную app для построения приложения.

3.4 HTML бутстрап

Для описания того, где приложение находится в DOM, а обычно это элемент <html>, нам надо связать атрибут ng-app с модулем. Так мы сообщаем Angular, куда подгрузить наше приложение.

<html ng-app="app">
  <head></head>
  <body></body>
</html>

Если мы грузим файлы с JavaScript асинхронно, нам надо подгрузить приложение вручную через angular.bootstrap(document.documentElement, [‘app’]);.

4 Разбираемся со $scope

Одно из основных понятий в программировании – область видимости. В Angular область видимости – это один из главных объектов, который делает возможным циклы двусторонней связи данных и сохраняет состояние приложения. $scope – довольно хитрый объект, который не только имеет доступ к данным и значениям, но и предоставляет эти данные в DOM, когда Angular рендерит наше приложение.

Представьте, что $scope – это автоматический мост между JavaScript и DOM, хранящий синхронизированные данные. Это позволяет проще работать с шаблонами, когда мы используем при этом синтакс HTML, а Angular рендерит соответствующие значения $scope. Это создаёт связь между JavaScript и DOM. В общем, $scope играет роль ViewModel.

$scope используется только внутри Контроллеров. Там мы привязываем данные Контроллера к Виду. Вот пример того, как мы объявляем данные в Контроллере:

$scope.someValue = 'Hello';

Чтобы это отобразилось в DOM, мы должны присоединить Контроллер к HTML и сообщить Angular, куда вставлять значение.

<div ng-controller="AppCtrl">
  {{ someValue }}
</div>

Перед вами концепция области видимости Angular, подчиняющаяся некоторым правилам JavaScript в плане лексических областей видимости. Снаружи элемента, к которому присоединён Контроллер, данные находятся вне области видимости – так же, как переменная вышла бы за область видимости, если б мы сослались на неё снаружи её области видимости.

Мы можем привязать любые типы JavaScript $scope. Таким образом мы берём данные от сервиса, общающегося с сервером, и передаём их во View, слой презентации.

Чем больше мы создадим Контроллеров и связей с данными, тем больше появляется областей видимости. Разобраться в их иерархии ничего не стоит – здесь нам поможет переменная $rootScope


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 с пустым элементом вроде <div></div>, который служит контейнером для любого динамически вставляемого кода, получаемого через 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 позволяет указывать на внешний ресурс или на элемент <script>, которые содержат нужный шаблон.

Если задать:

{
  templateUrl: 'items.html'
}

Angular сначала поищет в DOM элемент <script> с подходящим 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')
  .controller('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')
  .controller('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 в тэги <pre>

<pre>
{{ myObject | json }}
</pre>

 

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);

Комментарии запрещены.