Улучшить эту документациюСоздание пользовательских директивы
Примечание: это руководство предназначено для разработчиков, которые уже знакомы с основами AngularJS. Если вы только начинаете, мы рекомендуем сначала ознакомиться с руководством. Если вы ищете **API директивы**, мы недавно переместили его в$compile. В этом документе объясняется, когда вам нужно создать свои собственные директивы в приложении 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">
Следующее также **соответствует** ngModel:
<input data-ng-model="foo">
Нормализация
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 в качестве лучшего решения этой проблемы. Разработчики рекомендуют использовать это вместо пользовательских директив-комментариев, когда это возможно. Связывание текста и атрибутов
Во время процесса компиляции компилятор сопоставляет текст и атрибуты с помощью сервиса $interpolate, чтобы проверить, содержат ли они вложенные выражения. Эти выражения регистрируются как наблюдения и обновляются в рамках обычного цикла переваривания. Пример интерполяции показан ниже:
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
Связывание атрибутов ngAttr
Веб-браузеры иногда избирательны в отношении того, какие значения они считают допустимыми для атрибутов.
Например, рассмотрим этот шаблон:
<svg>
<circle cx="{{cx}}"></circle>
</svg>
Мы ожидаем, что Angular сможет привязаться к этому, но при проверке консоли мы видим что-то вроде Error: Invalid value for attribute cx="{{cx}}". Из-за ограничений API SVG DOM вы не можете просто написать cx="{{cx}}".
С помощью ng-attr-cx вы можете обойти эту проблему.
Если атрибут со связыванием имеет префикс ngAttr (денормализован как ng-attr-), то во время связывания он будет применен к соответствующему атрибуту без префикса. Это позволяет привязываться к атрибутам, которые в противном случае обрабатывались бы браузерами (например, атрибуты circle[cx] элемента SVG). При использовании ngAttr используется флаг allOrNothing сервиса $interpolate, поэтому, если какое-либо выражение в интерполированной строке приводит к undefined, атрибут удаляется и не добавляется к элементу.
Например, мы могли бы исправить пример выше, написав:
<svg>
<circle ng-attr-cx="{{cx}}"></circle>
</svg>
Если необходимо изменить атрибут с camelCase (элементы SVG имеют допустимые атрибуты с camelCase), например, viewBox для элемента svg, можно использовать подчеркивания для обозначения того, что привязываться следует к атрибуту с camelCase.
Например, чтобы привязаться к viewBox, мы можем написать:
<svg ng-attr-view_box="{{viewBox}}">
</svg>
Создание директив
Сначала давайте поговорим об 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'- соответствует только имени класса
Эти ограничения могут быть объединены по мере необходимости:
-
'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. link принимает функцию со следующим сигнатурой: function link(scope, element, attrs) { ... }, где:
-
scope– объект области действия Angular. -
element– элемент jqLite, соответствующий этой директиве. -
attrs– объект-словарь с парами имя-значение нормализованных имён атрибутов и соответствующих значений атрибутов.
В нашей функции 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.3.20/docs/guide/directive