Spec-Zone.ru › Angular.js 1.6

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

Примечание: это руководство предназначено для разработчиков, уже знакомых с основами 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 показывают в своей документации, какие типы сопоставления они поддерживают.

Следующий пример демонстрирует различные способы ссылки на директиву, которая соответствует всем 4 типам (в данном случае myDir), в шаблоне.

<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.6.9/docs/guide/directive

Spec-Zone.ru

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