Angular.JS: основы создания директив
концепции и практическое применение
Подготовка
Модульная структура
Как я уже писал ранее, AngularJS является достаточно структурированной библиотекой. Практически каждый элемент функциональности выделен в свой модуль: $http, $resource, $route, $location и так далее. Фактически сама библиотека сконцентрирована в модуле Core. Подключать его не нужно (как и многие другие модули, вроде $http), поскольку он входит в основу библиотеки.
Начиная с версии 1.1.6 модуль $route нужно подключать отдельно, поскольку было принято решение исключить его из ядра и впоследствии объединить с модулем ui.state от команды AngularUI.
Расширение функциональности
Следует заметить, что вызов angular.module
может работать по-разному в зависимости от переданных ему параметров. Если переданное первым параметром имя модуля соответствует уже существующему модулю, то вызов вернёт ссылку на этот модуль, если же такого модуля нет, то он предварительно будет создан.
Это позволяет держать код в разных файлах и не заботиться о последовательности их подключения/склейки. Достаточно лишь в начале каждого файла написать
|
|
Разумеется, можно подключать и зависимости, их наличие не влияет на работу функции angular.module
.
Настройка модуля
Каждый модуль ведёт себя как полноценный элемент приложения и сам может являться приложением (об этом говорилось ранее). Разумеется, что модули можно настраивать, меняя их поведение в зависимости от ситуации. Для этого предназначено две функции:
|
|
Провайдер
Провайдеры являются фабриками классов. Они создают готовые объекты, которые можно внедрять с помощью DI. Провайдеры являются основным способом расширения функциональности AngularJS. По своей сути, провайдер представляет собой объект, в котором находится единственная обязательная функция с строго регламентированным именем: $get
.
|
|
Помимо того, провайдер может включать любые методы, с помощью которых можно настроить создание объектов. Объекты, создаваемые провайдером, обычно называются сервисы.
Функция $get
вызывается инъектором в процессе внедрения зависимостей. Поэтому если написать её тем или иным способом, то можно получить разные результаты: например, всякий раз будет создаваться новый объект, а может и отдаваться ссылка на один и тот же общий. Второй вариант широко применяется для обмена данными между разными частями приложения/модуля.
Для доступа к самому провайдеру следует добавить к имени сервиса слово Provider
. Например, $httpProvider
. Следует заметить, что далеко не у всех сервисов есть свой отдельный провайдер, к которому можно получить доступ, как в примере выше.
Константа
Константа — это сервис, представляющий собой некую константу. Пример такого сервиса можно увидеть выше. Однако в AngularJS существуют функции для более удобного создания объектов, не имеющих отдельного провайдера, который не нужно настраивать. Для создания сервиса, отдающего некую константу, можно воспользоваться функцией module.value
, выглядит это так:
|
|
Эта запись эквивалентна следующей:
|
|
С той лишь разницей, что сокращённая запись не позволяет обращаться к TheAnswerProvider
за ненадобностью. В качестве константы выступать может что угодно, главное не забывать, что это всегда будет одно и то же значение. Попробуйте проверить, что будет, если в качестве константы задать объект и менять его свойства из разных частей приложения.
Фабрика
Фабрика это усложнённый вариант константы. Фабрика позволяет не только вернуть некоторое значение, но ещё и предварительно выполнить некоторые действия.
|
|
Обратите внимание, что я внедрил TheAsnwer в фабрику. Так же можно подключать любые зависимости. Сервисы могут и должны зависеть друг от друга.
Таким образом, я могу менять в некоторой степени поведение фабрики TheObject
, поскольку она зависит от TheAnswer
. Но лишь в некоторой степени.
Сервис
Сервис это фабрика, которая всякий раз возвращает новый объект. Иными словами, запись
|
|
Можно сократить до
|
|
Но что если в этом случае мы хотим менять передаваемый в процессе создания сервиса параметр? В этом случае нам и нужен полноценный провайдер.
В сущности, AngularJS все вышеописанные методы реализует через вызов module.provider
, они нужны лишь для удобства и сокращения записи. Полноценный сервис с провайдером же выглядит так:
|
|
|
|
Директивы
Хорошо, но что если нам нужно не только сделать какой-то сервис или модуль. Что если мы хотим реализовать что-то подобное директиве ngRepeat? Разумеется, AngularJS позволяет делать и это.
Рассмотрим, что AngularJS делает, когда встречает:
- Выражение
- DOM
$parse
Эта функция превращает любое допустимое выражение AngularJS в функцию. Эту функцию затем можно вызвать, передав в неё 1 или 2 параметра:
|
|
Вторым параметром можно передать локальные переменные, с помощью которых можно временно переопределить переменные внутри контекста — первого параметра.
Именно с помощью этой функции AngularJS и осуществляет связывание данных и вообще всё, что использует выражения.
В ваших приложениях эта функция вам почти никогда не понадобится, но знать о её существовании полезно.
$compile
$compile делает то же самое, что и $parse, но для HTML. Например
|
|
То есть это часть шаблонизатора AngularJS, осуществляющая привязку области видимости к шаблону.
Эта функция тоже вряд ли вам понадобится, но опять же знать о её существовании полезно.
Первая директива
Теперь, когда мы знаем, как AngularJS обрабатывает выражения и HTML, можно попробовать написать первую директиву. Я не буду описывать все возможные параметры, опишу лишь те, что обычно используются.
|
|
В результате работы этой директивы вместо <div>
будет выведено <p>Привет, Иван</p>
. Параметр replace
позволяет определить, будет ли директива целиком замещать DOM, которому применена, или же встраиваться внутрь него. Параметр template
можно заменить на templateUrl
и подключать шаблон из файла.
Наиболее важными параметрами здесь являются scope
и link
. Последний — это функция, осуществляющая привязку scope
к шаблону (см. выше про $compile
). Ну а scope
позволяет изолировать область видимости внутри директивы. Эти два параметра следует указывать практически всегда.
Есть также параметр compile
, который позволяет задать обработчик шаблона перед связыванием его с link
, но он используется довольно редко.
Процесс компиляции в AngularJS
- Сначала шаблон парсится стандартными средствами браузера. Важно понять, что шаблон должен быть допустимым HTML, иначе ничего не заработает.
- Вызывается
$compile
, который обрабатывает выражения и составляет список обнаруженных директив. Директивы для каждого тега сортируются в порядке важности (его можно указывать при разработке директивы), затем вызываются функцииcompile
у каждой из директив. В этих функциях директива имеет возможность изменить DOM по своему усмотрению. Результатом этого этапа будет одна общая функция линковки, включающая в себя также и все функцииlink
директив. - Вызывается функция, полученная на этапе 2, которая в свою очередь вызывает функции
link
всех директив, которые могут привязывать обработчики событий и т.д. - Получаем DOM с включенным двойным связыванием, который может динамически меняться.
Директива практически всегда имеет функцию link
и практически всегда не имеет функции compile
.
Полный код создания директивы
Хотя для простоты вы можете вообще возвращать в angular.directive
функцию link
:
|
|
Это используется довольно редко. Чаще всего используется вариант из раздела «Первая директива». Однако есть и максимально полный вариант.
|
|
Ограничение применимости
Поскольку в AngularJS существует несколько способов добавить директивы в DOM, вы можете отключить некоторые из них для вашей директивы. Если параметр restrict
не задан, то директивы можно добавлять лишь в качестве атрибутов к элементам HTML. Другие возможные значения выглядят так:
- E: только в качестве собственного элемента DOM:
<my-directive></my-directive>
- A: в качестве атрибута:
<div my-directive></div>
- C: в качестве CSS-класса:
<div class="my-directive: value"></div>
- M: в качестве комментария:
<!-- directive: my-directive value -->
Эти значения можно комбинировать, например так: restrict: "AC"
.
Изоляция области видимости
Изоляция области видимости обладает ещё одним крайне важным свойством: сокращение кода при получении параметров директивы. Рассмотрим уже известную директиву greet
.
|
|
Как видно, для извлечения нужного значения требуется проделать довольно некрасивую операцию. К счастью, в AngularJS это можно сделать намного проще.
|
|
Добавив к замыканию области видимости свойство, имя которого совпадает с именем атрибута (а директива это тоже атрибут), а значением является @
, можно автоматически передать значение атрибута в замыкание. Только обратите внимание, что в шаблоне имя переменной тоже поменялось. В таком простом случае функцию link
вообще можно удалить, что и было сделано.
Следует заметить, что если написать
|
|
И определить someName
где-то ещё, то директива заработает как и ожидается, но только в одну сторону. Можно поступить несколько иначе:
|
|
Такая запись позволяет осуществлять полноценное двойное связывание между директивой и внешним миром. Например, вы можете добавить <input ng-model='greet'>
в шаблон директивы и наблюдать, как someName
вне её будет успешно меняться при изменении значения в поле ввода.
Продвинутый вариант области видимости
Существует и ещё более продвинутый вариант изоляции области видимости, позволяющий не только связывать данные, но и передавать функциональность из внешнего контроллера в директиву.
|
|
Однако и это ещё не всё. Мы можем передавать параметры методу, который вызывается из контроллера. Делается это довольно необычно:
|
|
Важно Обратите внимание как передаются параметры в шаблоне директивы. Попробуйте создать две директивы и понаблюдать, как они будут работать независимо друг от друга:
|
|
Вводимые в поле ввода имена будут уникальными для каждой директивы и не помешают друг другу, несмотря на несколько смущающую запись. sayHello(name)
— параметр здесь это имя свойства объекта, передаваемого из директивы.
Всё вышеописанное легко достигается вообще без использования функции link
. Однако если вы хотите самостоятельно обрабатывать некие события браузера, она вам понадобится.
Что на самом деле делают @, =, &?
@ создаёт одностороннее связывание данных из родительской области видимости.
= позволяет изолированному в области видимости идентификатору участвовать в связывании (обратите внимание, как сильно это отличается от @)
& создаёт делегат. Если вы работали с C#, то знакомы с понятием делегата. Если же нет, то я рекомендую обратиться к другим источникам, хотя в общем-то для работы с AngularJS это и не обязательно.
Подмена
Предположим, что мы хотим создать директиву, показывающую диалоговое окно, внутри которого расположен произвольный HTML. В этом случае изоляции области видимости из атрибутов будет недостаточно. К счастью, AngularJS предлагает простую механику transclusion.
|
|
Я намеренно опустил логику создания диалога и сильно упростил итоговый HTML в template
. Для настоящей директивы понадобится добавить правильную разметку с кнопкой закрытия, кнопками действия (которые могут настраиваться через дополнительные атрибуты), повешать обработчики событий в функции link
и т.д. Однако этот пример позволяет быстро понять, как пользоваться функцией подмены разметки.
Важно: содержимое тега с директивой ngTransclude будет сохранено. Новая разметка будет добавлена к нему.
Взаимодействие директив
Директивы могут взаимодействовать друг с другом с помощью контроллеров. Одна директива может зависеть от другой и вызывать методы и свойства её контроллера.
|
|
Читайте так же статьи по теме:
- Улучшение логирования в AngularJS с помощью декораторов
- Ускоряем $digest цикл в AngularJS
- Последовательная асинхронная инициализация AngularJS приложений с использованием промисов
- “Безопасный” $apply в Angular.JS
- Работа с REST API в AngularJS
- Все о пользовательских фильтрах в AngularJS
- Как использовать ngMessages в AngularJS
- Data service для работы с API в AngularJS
- Последовательная асинхронная инициализация Angular.JS приложений с использованием промисов
- Дебаггинг приложения на AngularJS через консоль
- Разбираемся с системой событий $emit, $broadcast и $on в $scope и $rootScope Ангуляра
- Эффективное сквозное тестирование с Protractor. Часть 1