Улучшить эту документациюСоздание пользовательских директив
Примечание: это руководство предназначено для разработчиков, которые уже знакомы с основами AngularJS. Если вы только начинаете, мы рекомендуем начать с туториала. Если вы ищете API директив, вы можете найти его в$compile документации по API. В этом документе объясняется, когда вам следует создавать собственные директивы в приложении AngularJS, и как их реализовать.
Что такое директивы?
В общих чертах, директивы — это метки на элементе DOM (например, атрибут, имя элемента, комментарий или CSS-класс), которые сообщают компилятору HTML AngularJS ($compile) о том, как прикрепить к этому элементу DOM определённое поведение (например, через обработчики событий) или даже преобразовать элемент DOM и его потомков.
AngularJS поставляется с набором встроенных директив, таких как ngBind, ngModel, и ngClass. Подобно тому, как вы создаёте контроллеры и сервисы, вы можете создать свои собственные директивы для использования в AngularJS. Когда AngularJS инициализирует ваше приложение, компилятор HTML проходит по DOM, сопоставляя директивы с элементами DOM.
Сопоставление директив
Прежде чем мы сможем написать директиву, нам нужно знать, как компилятор HTML AngularJS определяет, когда использовать заданную директиву.
Аналогично терминологии, используемой при сопоставлении элемента со селектором, мы говорим, что элемент сопоставляется с директивой, когда директива является частью его объявления.
В следующем примере мы говорим, что элемент <input> сопоставляется с директивой ngModel
<input ng-model="foo">
Следующий элемент <input> также сопоставляется с ngModel:
<input data-ng-model="foo">
И следующий элемент <person> сопоставляется с директивой person:
<person>{{name}}</person>
Нормализация
AngularJS нормализует имя тега и имя атрибута элемента для определения, какие элементы соответствуют каким директивам. Обычно мы ссылаемся на директивы по их чувствительным к регистру camelCase нормализованным именам (например, ngModel). Однако, поскольку HTML нечувствителен к регистру, мы ссылаемся на директивы в DOM в нижнем регистре, обычно используя атрибуты с дефисами в элементах DOM (например, ng-model).
Процесс нормализации следующий:
- Удалить
x-иdata-с начала имени элемента/атрибута. - Преобразовать имя с разделителями
:,-, или_вcamelCase.
Например, следующие формы эквивалентны и соответствуют директиве ngBind:
ng-bind для ngBind). Если вы хотите использовать инструмент проверки HTML, вы можете использовать версию с префиксом data (например, data-ng-bind для ngBind). Другие представленные выше формы поддерживаются по причинам обратной совместимости, но мы рекомендуем их избегать. Типы директив
$compile может сопоставлять директивы на основе имен элементов (E), атрибутов (A), имен классов (C) и комментариев (M).
Встроенные директивы AngularJS отображаются на странице документации, какой тип сопоставления они поддерживают.
Следующий пример демонстрирует различные способы ссылки на директиву (myDir в данном случае), которая сопоставляется со всеми 4 типами, изнутри шаблона.
<my-dir></my-dir> <span my-dir="exp"></span> <!-- directive: my-dir exp --> <span class="my-dir: exp;"></span>
Директива может указать, какие из 4 типов сопоставления она поддерживает, в свойстве restrict объекта определения директивы. По умолчанию это EA.
<table>). AngularJS 1.2 вводит ng-repeat-start и ng-repeat-end в качестве лучшего решения этой проблемы. Разработчикам рекомендуется использовать этот подход вместо пользовательских директив комментариев, когда это возможно. Создание директив
Сначала давайте поговорим о API для регистрации директив. Как и контроллеры, директивы регистрируются в модулях. Для регистрации директивы вы используете API module.directive. module.directive принимает нормализованное имя директивы, за которым следует функция-фабрика. Эта функция-фабрика должна возвращать объект с различными параметрами для того, чтобы $compile знало, как директива должна вести себя при сопоставлении.
Функция-фабрика вызывается только один раз при первом сопоставлении директивы компилятором. Здесь вы можете выполнить любую инициализацию. Функция вызывается с помощью $injector.invoke, что делает её инъекционной, как и контроллер.
Мы рассмотрим несколько общих примеров директив, а затем подробно разберем различные параметры и процесс компиляции.
<carousel>, возникнет проблема, если HTML7 введёт элемент с таким же именем. Префикс из двух или трёх букв (например, btfCarousel) подойдёт хорошо. Аналогично, не используйте префиксы ng для своих директив, так как они могут конфликтовать с директивами, включёнными в будущие версии AngularJS. В следующих примерах мы будем использовать префикс my (например, myCustomer).
Директива расширения шаблона
Предположим, у вас есть фрагмент шаблона, представляющий информацию о клиенте. Этот шаблон многократно повторяется в вашем коде. При изменении в одном месте вам приходится изменять его в нескольких других. Это хороший случай для использования директивы для упрощения вашего шаблона.
Давайте создадим директиву, которая просто заменяет свой контент статическим шаблоном:
Обратите внимание, что в этой директиве есть связи. После того как $compile скомпилирует и подключит <div my-customer></div>, она будет пытаться сопоставить директивы в дочерних элементах. Это означает, что вы можете комбинировать директивы других директив. Мы увидим, как это сделать в примере ниже.
В приведённом выше примере значение параметра template задано непосредственно, но это станет неудобно по мере увеличения размера вашего шаблона.
templateUrl.Если вы знакомы с ngInclude, templateUrl работает аналогично. Вот тот же пример с использованием templateUrl вместо него:
templateUrl также может быть функцией, возвращающей URL HTML-шаблона для загрузки и использования в директиве. AngularJS вызовет функцию templateUrl с двумя параметрами: элементом, на который была вызвана директива, и объектом attr (scope), связанным с этим элементом.
templateUrl, так как шаблон запрашивается до инициализации области видимости. restrict.Параметр restrict обычно устанавливается в:
-
'A'- соответствует только имени атрибута -
'E'- соответствует только имени элемента -
'C'- соответствует только имени класса -
'M'- соответствует только комментарию
Эти ограничения могут быть объединены по мере необходимости:
-
'AEC'- соответствует либо имени атрибута, либо имени элемента, либо имени класса
Давайте изменим нашу директиву, чтобы использовать restrict: 'E':
Дополнительную информацию о свойстве restrict можно найти в документации по API.
Использование элемента для директивы myCustomer — очевидно правильный выбор, так как вы не дополняете элемент каким-то поведением «клиент»; вы определяете основное поведение элемента как компонента «клиента».
Изоляция области видимости директивы
Наша директива myCustomer выше отличная, но у неё есть серьёзный недостаток. Мы можем использовать её только один раз в данной области видимости.
В текущей реализации нам нужно создать отдельный контроллер каждый раз для повторного использования такой директивы:
Это явно не лучшее решение.
Мы хотим иметь возможность отделить область действия директивы от области действия внешнего кода, а затем сопоставить внешнюю область действия с внутренней областью действия директивы. Это можно сделать, создав так называемую изолированную область действия. Для этого можно использовать опцию директивы scope:
Рассмотрим index.html, первый <my-customer> элемент связывает атрибут info с naomi, который мы экспонировали в области действия нашего контроллера. Второй связывает info с igor.
Давайте подробнее рассмотрим опцию области действия:
//...
scope: {
customerInfo: '=info'
},
//...
Опция scope — это объект, содержащий свойство для каждой связи изолированной области действия. В данном случае у него есть только одно свойство:
- Его имя (
customerInfo) соответствует свойству изолированной области действия директивы,customerInfo. - Его значение (
=info) указывает на то, что нужно привязать к атрибуту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— объект области действия AngularJS. -
element— элемент jqLite, которому соответствует эта директива. -
attrs— хеш-объект со списком пар «имя атрибута» — «значение атрибута». -
controller— экземпляр(ы) требуемого контроллера или собственный контроллер (если таковой имеется). Точное значение зависит от свойства require директивы. -
transcludeFn— функция связывания трансляции, предварительно привязанная к правильной области действия трансляции.
link см. на странице API $compile. В нашей функции link мы хотим обновлять отображаемое время раз в секунду или всякий раз, когда пользователь меняет строку форматирования времени, к которой наша директива привязана. Мы будем использовать сервис $interval для вызова обработчика на регулярной основе. Это проще, чем использование $timeout, но также лучше работает с end-to-end-тестированием, где мы хотим убедиться, что все $timeout завершены перед завершением теста. Мы также хотим удалить $interval при удалении директивы, чтобы не создавать утечку памяти.
Здесь есть несколько моментов. Так же, как и в API module.controller, аргумент функции в module.directive зависит от инъекции зависимостей. Благодаря этому, мы можем использовать $interval и dateFilter внутри функции link нашей директивы.
Мы регистрируем событие element.on('$destroy', ...). Что вызывает это событие $destroy?
AngularJS испускает несколько специальных событий. При уничтожении узла DOM, который был скомпилирован с помощью компилятора AngularJS, он испускает событие $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 принимает сообщение для отображения при скрытии диалогового окна. Это указывается в директиве путём вызова 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–2020 Google, Inc.
Licensed under the Creative Commons Attribution License 3.0.
https://code.angularjs.org/1.8.2/docs/guide/directive