Улучшить эту страницуСоздание пользовательских директив
Примечание: это руководство предназначено для разработчиков, которые уже знакомы с основами AngularJS. Если вы только начинаете, мы рекомендуем начать с туториала. Если вы ищете API директив, вы можете найти его в$compile документации API. В этом документе объясняется, когда вам нужно создавать собственные директивы в вашем приложении AngularJS и как их реализовать.
Что такое директивы?
В общем случае, директивы — это метки на элементе DOM (например, атрибут, имя элемента, комментарий или класс CSS), которые сообщают компилятору HTML AngularJS ($compile) о прикреплении определённого поведения к этому элементу DOM (например, через обработчики событий) или даже о преобразовании элемента DOM и его потомков.
Angular поставляется с набором встроенных директив, таких как ngBind, ngModel, и ngClass. Подобно тому, как вы создаёте контроллеры и сервисы, вы можете создавать свои собственные директивы для использования в Angular. Когда Angular инициализирует ваше приложение, компилятор HTML проходит по DOM, сопоставляя директивы с элементами DOM.
Сопоставление директив
Прежде чем мы сможем написать директиву, нам нужно знать, как компилятор HTML Angular определяет, когда использовать заданную директиву.
Аналогично терминологии, используемой при сопоставлении элемента с селектором, мы говорим, что элемент сопоставляется с директивой, когда директива является частью его объявления.
В следующем примере мы говорим, что элемент <input> сопоставляется с директивой ngModel
<input ng-model="foo">
Следующий элемент <input> также сопоставляется с ngModel
<input data-ng-model="foo">
И следующий элемент <person> сопоставляется с директивой person
<person>{{name}}</person>
Нормализация
Angular нормализует имя элемента и атрибута для определения, какие элементы соответствуют каким директивам. Обычно мы ссылаемся на директивы по их чувствительному к регистру camelCase нормализованному имени (например, ngModel). Однако, поскольку HTML нечувствителен к регистру, мы ссылаемся на директивы в DOM по строчным формам, обычно используя разделенные дефисами атрибуты в элементах DOM (например, ng-model).
Процесс нормализации следующий:
- Удалить
x-иdata-с начала элемента/атрибутов. - Преобразовать имя, разделенное
:,-, или_, вcamelCase.
Например, следующие формы эквивалентны и соответствуют директиве ngBind:
ng-bind для ngBind). Если вы хотите использовать инструмент проверки HTML, вы можете вместо этого использовать префикс data (например, data-ng-bind для ngBind). Другие формы, показанные выше, принимаются по соображениям совместимости, но мы рекомендуем их избегать. Типы директив
$compile может сопоставлять директивы на основе имён элементов, атрибутов, имён классов и комментариев.
Все предоставленные Angular директивы сопоставляются по имени атрибута, имени тега, комментариям или имени класса. Следующий пример демонстрирует различные способы ссылки на директиву (myDir в данном случае) из шаблона:
<my-dir></my-dir> <span my-dir="exp"></span> <!-- directive: my-dir exp --> <span class="my-dir: exp;"></span>
<table>). AngularJS 1.2 вводит ng-repeat-start и ng-repeat-end как лучшее решение этой проблемы. Разработчикам рекомендуется использовать это вместо пользовательских директив с комментариями, когда это возможно. Создание директив
Сначала давайте поговорим о API для регистрации директив. Подобно контроллерам, директивы регистрируются в модулях. Для регистрации директивы используется API module.directive. module.directive принимает нормализованное имя директивы, за которым следует функция-фабрика. Эта функция-фабрика должна возвращать объект с различными параметрами, чтобы сообщить $compile о том, как директива должна себя вести при сопоставлении.
Функция-фабрика вызывается только один раз, когда компилятор впервые находит директиву. Здесь вы можете выполнить любую инициализацию. Функция вызывается с помощью $injector.invoke, что делает её инъектируемой, как и контроллер.
Мы рассмотрим несколько распространенных примеров директив, а затем углубимся в различные параметры и процесс компиляции.
<carousel>, это будет проблематично, если HTML7 введёт элемент с таким же именем. Префикс из двух или трёх букв (например, btfCarousel) хорошо подходит. Аналогично, не префиксеруйте свои директивы с ng, так как они могут конфликтовать с директивами, включёнными в будущие версии Angular. В следующих примерах мы будем использовать префикс my (например, myCustomer).
Директива расширения шаблона
Предположим, у вас есть часть шаблона, которая представляет информацию о клиенте. Этот шаблон повторяется много раз в вашем коде. Когда вы изменяете его в одном месте, вам нужно изменить его в нескольких других. Это хороший случай для использования директивы, чтобы упростить ваш шаблон.
Давайте создадим директиву, которая просто заменяет своё содержимое статическим шаблоном:
Обратите внимание, что в этой директиве есть связи. После того, как $compile скомпилирует и свяжет <div my-customer></div>, она будет пытаться сопоставить директивы в потомках элемента. Это означает, что вы можете комбинировать директивы других директив. Мы покажем, как это сделать в примере ниже.
В примере выше мы встроили значение опции template, но это станет утомительным по мере роста вашего шаблона.
templateUrl.Если вы знакомы с ngInclude, templateUrl работает так же. Вот тот же пример с использованием templateUrl:
templateUrl также может быть функцией, которая возвращает URL HTML-шаблона для загрузки и использования в директиве. Angular вызовет функцию templateUrl с двумя параметрами: элементом, на котором вызвана директива, и объектом attr , связанным с этим элементом.
templateUrl , так как шаблон запрашивается до инициализации области видимости. restrict.Опция restrict обычно устанавливается на:
-
'A'— сопоставляется только по имени атрибута -
'E'— сопоставляется только по имени элемента -
'C'— сопоставляется только по имени класса -
'M'— сопоставляется только по комментарию
Эти ограничения могут быть объединены по мере необходимости:
-
'AEC'— сопоставляется по имени атрибута, имени элемента или имени класса
Давайте изменим нашу директиву, чтобы использовать restrict: 'E':
Дополнительную информацию об свойстве restrict можно найти в документации API.
Использование элемента для директивы myCustomer — это, безусловно, правильный выбор, потому что вы не дополняете элемент каким-то поведением "клиента"; вы определяете основное поведение элемента как компонента клиента.
Изоляция области видимости директивы
Наша директива myCustomer выше отличная, но у неё есть существенный недостаток. Мы можем использовать её только один раз в данной области видимости.
В текущей реализации нам нужно создать отдельный контроллер каждый раз, чтобы повторно использовать такую директиву:
Это, очевидно, не очень хорошее решение.
То, что мы хотим сделать, — это отделить область видимости внутри директивы от области видимости снаружи и затем сопоставить внешнюю область видимости с внутренней областью видимости директивы. Мы можем сделать это, создав так называемую изолированную область видимости. Для этого мы можем использовать опцию директивы scope.
Рассматривая index.html, первый элемент <my-customer> связывает атрибут info с naomi, который мы предоставили в области видимости нашего контроллера. Второй связывает info с igor.
Давайте более подробно рассмотрим параметр scope:
//...
scope: {
customerInfo: '=info'
},
//...
Параметр scope — это объект, содержащий свойство для каждого связывания с изолированной областью видимости. В данном случае он содержит только одно свойство:
- Его имя (
customerInfo) соответствует свойству изолированной области видимости директивы,customerInfo. - Его значение (
=info) указывает, чтобы$compileпривязался к атрибутуinfo.
=attr атрибуты в параметре scope директив нормируются так же, как и имена директив. Чтобы привязаться к атрибуту в <div bind-to-this="thing">, вы бы указали привязку =bindToThis. В случаях, когда имя атрибута совпадает со значением, к которому вы хотите привязаться внутри области видимости директивы, можно использовать этот сокращённый синтаксис:
...
scope: {
// same as '=customer'
customer: '='
},
...
Помимо возможности привязывать различные данные к области видимости внутри директивы, использование изолированной области видимости имеет ещё один эффект.
Мы можем продемонстрировать это, добавив ещё одно свойство, vojta, в нашу область видимости и попытаемся получить доступ к нему из шаблона нашей директивы:
Обратите внимание, что {{vojta.name}} и {{vojta.address}} пустые, что означает, что они не определены. Хотя мы определили vojta в контроллере, оно недоступно внутри директивы.
Как следует из названия, изолированная область видимости директивы изолирует всё, кроме моделей, которые вы явно добавили в объект хэша scope: {}. Это полезно при создании переиспользуемых компонентов, так как это предотвращает изменение состояния вашей модели компонентом, кроме тех моделей, которые вы явно передали.
scope для создания изолированных областей видимости при создании компонентов, которые вы хотите повторно использовать в приложении. Создание директивы, которая манипулирует DOM
В этом примере мы создадим директиву, которая отображает текущее время. Каждую секунду она обновляет DOM, чтобы отразить текущее время.
Директивы, которые хотят изменить DOM, обычно используют параметр link для регистрации слушателей DOM, а также для обновления DOM. Он выполняется после клонирования шаблона и является местом, где будет размещена логика директивы.
link принимает функцию со следующей сигнатурой, function link(scope, element, attrs, controller, transcludeFn) { ... }, где:
-
scope— объект области видимости Angular. -
element— элемент jqLite, которому соответствует данная директива. -
attrs— объект хэша с парами "ключ-значение" нормированных имён атрибутов и соответствующих им значений атрибутов. -
controller— требуемый(ые) экземпляр(ы) контроллера директивы или её собственный контроллер (если есть). Точное значение зависит от свойства require директивы. -
transcludeFn— функция связывания трансклюзии, предварительно привязанная к правильной области видимости трансклюзии.
link обратитесь к странице $compile API. В нашей функции link мы хотим обновлять отображаемое время каждую секунду или всякий раз, когда пользователь изменяет строку форматирования времени, к которому наша директива привязана. Мы будем использовать службу $interval для вызова обработчика регулярно. Это проще, чем использование $timeout, но также лучше работает с тестированием на стыке (end-to-end), где мы хотим убедиться, что все $timeout завершились до завершения теста. Мы также хотим удалить $interval при удалении директивы, чтобы не допустить утечки памяти.
Здесь стоит отметить несколько моментов. Так же, как и в API module.controller, аргумент функции в module.directive подключается по зависимостям. Благодаря этому, мы можем использовать $interval и dateFilter внутри функции link нашей директивы.
Мы регистрируем событие element.on('$destroy', ...). Что запускает это событие $destroy?
AngularJS генерирует несколько специальных событий. Когда узел DOM, который был скомпилирован с помощью компилятора Angular, уничтожается, он генерирует событие $destroy. Аналогично, когда область видимости AngularJS уничтожается, она транслирует событие $destroy в прослушивающие области видимости.
Прослушивая это событие, вы можете удалить слушатели событий, которые могут вызвать утечку памяти. Слушатели, зарегистрированные в областях видимости и элементах, автоматически удаляются при их уничтожении, но если вы зарегистрировали слушатель на службе или зарегистрировали слушатель на узле DOM, который не удаляется, вам нужно будет удалить его самостоятельно, чтобы избежать утечки памяти.
element.on('$destroy', ...) или scope.$on('$destroy', ...) для запуска функции очистки при удалении директивы. Создание директивы, которая оборачивает другие элементы
Мы видели, как можно передавать модели в директиву с помощью изолированной области видимости, но иногда бывает желательно передавать целую шаблон, а не строку или объект. Предположим, что мы хотим создать компонент «окно диалога». Окно диалога должно быть способно оборачивать любое произвольное содержимое.
Для этого нам нужно использовать параметр transclude.
Что именно делает этот параметр transclude? transclude делает так, чтобы содержимое директивы с этим параметром имело доступ к области видимости вне директивы, а не внутри.
Чтобы проиллюстрировать это, см. пример ниже. Обратите внимание, что мы добавили функцию link в script.js, которая переопределяет name как Jeff. Как вы думаете, к чему теперь будет привязано {{name}}?
Обычно мы ожидали бы, что {{name}} будет Jeff. Однако, в этом примере мы видим, что привязка {{name}} по-прежнему Tobias.
Параметр transclude изменяет способ вложенности областей видимости. Это делает так, что содержимое трансклюдированной директивы имеет ту область видимости, которая находится вне директивы, а не ту, которая находится внутри. Таким образом, оно предоставляет содержимому доступ к внешней области видимости.
Обратите внимание, что если директива не создавала свою собственную область видимости, то scope в scope.name = 'Jeff' будет ссылаться на внешнюю область видимости, и мы увидим Jeff в выводе.
Это поведение логично для директивы, которая оборачивает некоторое содержимое, так как в противном случае вам пришлось бы передавать каждую модель, которую вы хотите использовать отдельно. Если вам нужно передавать каждую модель, которую вы хотите использовать, то у вас не может быть произвольного содержимого, не так ли?
transclude: true только тогда, когда вы хотите создать директиву, которая оборачивает произвольное содержимое. Далее, мы хотим добавить кнопки в это окно диалога и позволить кому-либо, использующему директиву, привязать своё собственное поведение к ней.
Мы хотим запустить функцию, которую мы передаём, вызвав её из области видимости директивы, но выполнить её в контексте области видимости, в которой она зарегистрирована.
Мы видели ранее, как использовать =attr в параметре scope , но в примере выше, мы используем &attr вместо этого. Связывание & позволяет директиве инициировать оценку выражения в контексте исходной области видимости в определённое время. Разрешается любое допустимое выражение, в том числе выражение, которое содержит вызов функции. Благодаря этому, привязки & идеально подходят для привязки функций обратного вызова к поведению директивы.
Когда пользователь нажимает на x в диалоговом окне, вызывается функция close директивы, благодаря ng-click. Этот вызов close в изолированной области видимости фактически оценивает выражение hideDialog(message) в контексте исходной области видимости, тем самым вызывая функцию Controller hideDialog.
Часто бывает желательно передавать данные из изолированной области видимости через выражение в родительскую область видимости, это можно сделать, передав карту имён и значений локальных переменных в функцию обертки выражения. Например, функция hideDialog принимает сообщение для отображения при скрытии диалога. Это указывается в директиве вызовом close({message: 'closing for now'}). Затем локальная переменная message будет доступна в выражении on-close.
&attr в параметре scope когда вы хотите, чтобы ваша директива предоставляла API для привязки к поведению. Создание директивы, которая добавляет слушателей событий
Ранее мы использовали функцию link для создания директивы, которая манипулировала своими элементами DOM. Исходя из этого примера, давайте создадим директиву, которая реагирует на события в своих элементах.
Например, что если мы хотим создать директиву, которая позволяет пользователю перетаскивать элемент?
Создание директивы, которые взаимодействуют
Вы можете комбинировать любые директивы, используя их в шаблонах.
Иногда вам нужен компонент, который создаётся из комбинации директив.
Представьте, что вы хотите иметь контейнер с вкладками, содержимое которого соответствует активной вкладке.
Директива myPane имеет параметр require со значением ^^myTabs. Когда директива использует этот параметр, $compile выдаст ошибку, если указанный контроллер не найден. Префикс ^^ означает, что эта директива ищет контроллер у своих родителей. (Префикс ^ заставит директиву искать контроллер в собственном элементе или у своих родителей; без префикса директива будет искать только в собственном элементе.)
Итак, откуда берётся этот контроллер myTabs? Директивы могут указывать контроллеры, используя параметр controller . Как вы можете видеть, директива myTabs использует этот параметр. Как и ngController, этот параметр прикрепляет контроллер к шаблону директивы.
Если необходимо сослаться на контроллер или любые функции, связанные с контроллером, из шаблона, вы можете использовать опцию controllerAs для указания имени контроллера как псевдонима. Директива должна определить область видимости для использования этой конфигурации. Это особенно полезно в случае, когда директива используется как компонент.
Обращаясь к определению myPane, обратите внимание на последний аргумент в его функции link: tabsCtrl. Когда директива требует контроллер, она получает этот контроллер в качестве четвертого аргумента своей функции link. Используя это, myPane может вызвать функцию addPane контроллера myTabs.
Если требуется несколько контроллеров, опция require директивы может принимать массив аргументов. Соответствующий параметр, отправляемый в функцию link, также будет массивом.
angular.module('docsTabsExample', [])
.directive('myPane', function() {
return {
require: ['^^myTabs', 'ngModel'],
restrict: 'E',
transclude: true,
scope: {
title: '@'
},
link: function(scope, element, attrs, controllers) {
var tabsCtrl = controllers[0],
modelCtrl = controllers[1];
tabsCtrl.addPane(scope);
},
templateUrl: 'my-pane.html'
};
});
Внимательные читатели могут задаться вопросом, в чем разница между link и controller. Основное различие заключается в том, что controller может экспонировать API, а функции link могут взаимодействовать с контроллерами, используя require.
controller когда вы хотите экспонировать API для других директив. В противном случае используйте link. Обзор
Здесь мы рассмотрели основные варианты использования директив. Каждый из этих примеров служит хорошей отправной точкой для создания собственных директив.
Возможно, вас также заинтересует подробное объяснение процесса компиляции, которое доступно в руководстве по компилятору.
На странице $compile API представлен исчерпывающий список опций директивы для справки.
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.5.11/docs/guide/directive