Spec-Zone.ru › Angular.js 1.7

Улучшить эту статьюСоздание пользовательских директив

Примечание: данное руководство предназначено для разработчиков, уже знакомых с основами AngularJS. Если вы только начинаете, рекомендуем начать с руководства. Если вам нужен API директив, вы можете найти его в $compile документации по API.

В этом документе объясняется, когда необходимо создавать собственные директивы в приложении AngularJS, и как их реализовать.

Что такое директивы?

В общем случае директивы — это метки на элементе DOM (например, атрибут, имя элемента, комментарий или класс CSS), которые сообщают компилятору HTML AngularJS ($compile) о необходимости прикрепить к этому элементу DOM указанное поведение (например, через обработчики событий) или даже преобразовать элемент DOM и его потомков.

AngularJS поставляется со встроенным набором директив, таких как ngBind, ngModel, и ngClass. Подобно тому, как вы создаёте контроллеры и сервисы, вы можете создавать свои собственные директивы для использования в AngularJS. Когда AngularJS инициализирует ваше приложение, компилятор HTML проходит по DOM, сопоставляя директивы с элементами DOM.

Что означает «компиляция» HTML-шаблона? Для AngularJS «компиляция» означает прикрепление директив к HTML для создания интерактивности. Мы используем термин «компиляция», поскольку рекурсивный процесс прикрепления директив отражает процесс компиляции исходного кода в компилируемых языках программирования.

Сопоставление директив

Прежде чем мы сможем написать директиву, нам нужно знать, как компилятор 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).

Процесс нормализации выполняется следующим образом:

  1. Удалить x- и data- из начала элемента/атрибутов.
  2. Преобразовать имя с :, -, или _-разделением в 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.

Лучшая практика: Предпочитайте использовать директивы через имя тега и атрибуты, а не имена классов и комментарии. Это обычно упрощает определение, какие директивы соответствует данный элемент.
Лучшая практика: Директивы комментариев часто использовались в ситуациях, когда API DOM ограничивал возможность создания директив, охватывающих несколько элементов (например, внутри элементов <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, но это станет утомительным по мере увеличения размера шаблона.

Лучшая практика: Если ваш шаблон не очень мал, лучше разбить его на отдельные HTML-файлы и загрузить их с помощью опции templateUrl.

Если вы знакомы с ngInclude, templateUrl работает точно так же. Вот тот же пример, использующий templateUrl:

templateUrl также может быть функцией, которая возвращает URL HTML-шаблона для загрузки и использования в директиве. AngularJS вызовет функцию 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: {
  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".
Рекомендация: Используйте опцию 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–2018 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.7.8/docs/guide/directive

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API