Spec-Zone.ru › Angular.js 1.2

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

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

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

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

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

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

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

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

Прежде чем писать директиву, нам нужно знать, как компилятор HTML Angular определяет, когда использовать данную директиву.

В следующем примере мы говорим, что элемент <input> соответствует директиве ngModel.

<input ng-model="foo">

Следующее также соответствует ngModel:

<input data-ng:model="foo">

Angular нормализует имя тега и атрибута элемента для определения, какие элементы соответствуют каким директивам. Обычно мы ссылаемся на директивы по их чувствительному к регистру camelCase нормализованному имени (например, ngModel). Однако, поскольку HTML нечувствителен к регистру, мы ссылаемся на директивы в DOM с помощью строчных форм, обычно используя разделенные дефисами атрибуты в элементах DOM (например, ng-model).

Процесс нормализации следующий:

  1. Удалить x- и data- из начала элемента/атрибутов.
  2. Преобразовать имя, разделенное :, -, или _ в 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>
Рекомендация: Предпочитайте использовать директивы через имя тега и атрибуты вместо комментариев и имён классов. Это обычно упрощает определение директив, которым соответствует данный элемент.
Рекомендация: Директивы в виде комментариев часто использовались в тех местах, где API DOM ограничивает возможность создания директив, охватывающих несколько элементов (например, внутри элементов <table>). AngularJS 1.2 вводит ng-repeat-start и ng-repeat-end в качестве лучшего решения этой проблемы. Разработчикам рекомендуется использовать этот вариант вместо пользовательских директив в виде комментариев, когда это возможно.

Текстовые и атрибутные привязки

Во время процесса компиляции компилятор использует службу $interpolate для сопоставления текста и атрибутов и определения, содержат ли они вложенные выражения. Эти выражения регистрируются как наблюдения и обновляются в рамках обычного цикла digest. Ниже показан пример интерполяции:

<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 DOM SVG вы не можете просто написать cx="{{cx}}".

С помощью ng-attr-cx вы можете обойти эту проблему.

Если атрибут с привязкой имеет префикс ngAttr (денормализованный как ng-attr-), то во время привязки он будет применён к соответствующему атрибуту без префикса. Это позволяет вам привязываться к атрибутам, которые в противном случае были бы немедленно обработаны браузерами (например, атрибуты circle[cx] элемента SVG).

Например, мы можем исправить пример выше, написав:

<svg>
<circle ng-attr-cx="{{cx}}"></circle>
</svg>

Создание Директив

Сначала давайте поговорим о API для регистрации директив. Как и контроллеры, директивы регистрируются в модулях. Для регистрации директивы вы используете API module.directive. module.directive принимает нормализованное имя директивы, за которым следует функция-фабрикатор. Эта функция-фабрикатор должна возвращать объект с различными опциями, чтобы сообщить $compile, как директива должна себя вести при соответствии.

Функция-фабрикатор вызывается только один раз, когда компилятор $compile впервые находит соответствие для директивы. Здесь вы можете выполнить любые работы по инициализации. Функция вызывается с помощью $injector.invoke, что делает её инъектируемой, как и контроллер.

Рекомендация: Предпочитайте использовать объект определения вместо возвращаемой функции.

Мы рассмотрим несколько распространённых примеров директив, а затем углубимся в различные опции и процесс компиляции.

Рекомендация: Чтобы избежать конфликтов с будущими стандартами, лучше префиксровать свои собственные имена директив. Например, если вы создали директиву <carousel> , будет проблематично, если HTML7 введёт тот же элемент. Префикс из двух или трёх букв (например, btfCarousel) хорошо подходит. Аналогично, не префиксруйте свои собственные директивы с ng, иначе они могут конфликтовать с директивами, включёнными в будущие версии Angular.

Для следующих примеров мы будем использовать префикс my (например, myCustomer).

Директива расширения шаблона

Предположим, у вас есть часть шаблона, представляющая информацию о клиенте. Этот шаблон повторяется многократно в вашем коде. Когда вы изменяете его в одном месте, вам нужно изменить его в нескольких других. Это хорошая возможность использовать директиву для упрощения шаблона.

Давайте создадим директиву, которая просто заменяет свой контент статическим шаблоном:

Обратите внимание, что в этой директиве есть привязки. После того, как $compile скомпилирует и свяжет <div my-customer></div>, он попытается найти директивы в дочерних элементах. Это означает, что вы можете комбинировать директивы других директив. Мы увидим, как это сделать в примере ниже.

В примере выше мы встроили значение опции template , но это станет неудобно по мере роста вашего шаблона.

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

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

Отлично! Но что, если мы захотим, чтобы наша директива соответствовала имени тега <my-customer> вместо этого? Если мы просто поместим элемент <my-customer> в HTML, он не работает.

Примечание: При создании директивы она по умолчанию ограничена только атрибутами. Чтобы создать директивы, срабатывающие по имени элемента или классу, необходимо использовать опцию restrict .

Опция restrict обычно устанавливается в:

  • 'A' - соответствует только имени атрибута
  • 'E' - соответствует только имени элемента
  • 'C' - соответствует только имени класса

Эти ограничения могут быть объединены по мере необходимости:

  • 'AEC' - соответствует имени атрибута, имени элемента или имени класса

Давайте изменим нашу директиву, чтобы использовать restrict: 'E':

Дополнительную информацию об свойстве restrict см. в документации API.

Когда следует использовать атрибут вместо элемента? Используйте элемент, когда вы создаёте компонент, который управляет шаблоном. Типичный случай — создание языка предметной области для частей шаблона. Используйте атрибут, когда вы дополняете существующий элемент новой функциональностью.

Использование элемента для директивы myCustomer является правильным выбором, поскольку вы не дополняете элемент поведением «клиента»; вы определяете основное поведение элемента как компонента клиента.

Изоляция области действия директивы

Наша директива myCustomer выше отлична, но у неё есть фатальный недостаток. Мы можем использовать её только один раз в данном области видимости.

В её текущей реализации нам нужно создавать новый контроллер каждый раз для повторного использования такой директивы:

Это явно не очень хорошее решение.

То, что мы хотим иметь возможность сделать, это отделить область видимости внутри директивы от области видимости снаружи, а затем сопоставить внешнюю область видимости с внутренней областью видимости директивы. Мы можем сделать это, создав то, что мы называем изолированной областью видимости. Для этого мы можем использовать опцию scope директивы:

Глядя на index.html, первый элемент <my-customer> связывает атрибут info с naomi, который мы предоставили в области видимости нашего контроллера. Второй связывает info с igor.

Давайте подробнее рассмотрим опцию области видимости:

//...
scope: {
  customerInfo: '=info'
},
//...

Опция области видимости — это объект, содержащий свойство для каждой связи изолированной области видимости. В данном случае у него только одно свойство:

  • Его имя (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, но также лучше работает с тестированием «от края до края», где мы хотим убедиться, что все $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() в контексте исходной области видимости, тем самым вызывая функцию Controller hideDialog.

Рекомендация: используйте &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.2.32/docs/guide/directive

Spec-Zone.ru

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