Директивы — это ключевая особенность AngularJS. С помощью директив можно добавлять новое поведение существующим HTML элементам, можно создавать новые компоненты. Примерами директив, добавляющих новое поведения для существующих HTML элементов, могут служить input, select, textarea в связке с ngModel, required и т.п. Перечисленные директивы в основном связаны с валидацией форм в AngularJS. Но тема валидации заслуживает отдельной статьи.
Директивы можно и нужно использовать для повышения модульности вашего приложения, выделения обособленной функциональности в компоненты, в том числе и для повторного использования.
Если вы разрабатываете приложение на AngularJS и не создаете директивы, то это уже само по себе немного настораживает. Либо ваше приложение достаточно простое и уложилось в стандартные возможности AngularJS, либо, скорее всего, что-то не так с архитектурой вашего приложения. А если у вас при этом есть работа с DOM-ом в контроллерах или сервисах, то вам однозначно надо разбираться с темой создания директив, т.к. манипуляций с DOM-ом не должно быть нигде, кроме директив.
В данной статье я постараюсь рассмотреть процесс создания собственных директив на нескольких примерах.
Хорошим примером создания директив могут служить репозитарии команды AngularUI. В эту команду входят разработчики, не являющиеся сотрудниками Google, но очень хорошо зарекомендовавшие себя в списке рассылки и на stackoverflow. Насколько я могу судить, они создают production-ready компоненты с настройками, покрывающими большую часть вариантов использования. У меня тоже есть репозиторий, в который я выкладываю некоторые свои наработки. Но у меня немного другой подход. Мне больше нравится делать директивы под конкретные варианты использования. AngularJS очень лаконичен. Меньше кода => лучше читаемость => проще поддержка и изменение. Зачем тогда создавать «монструозные» компоненты с кучей настроек? Поэтому рассматривайте эти директивы как отправную точку для создания своих собственных под конкретные нужды. Еще за примерами можно пойти на сайт ngmodules.org, возможно, он сможет стать каталогом различных компонентов для AngularJS.
Итак, базовым документом для разработки своих директив является статья Directives из Developer Guide. Там все расписано очень хорошо и подробно. К этому документу придется возвращаться еще не раз.
Директива-обертка для Tooltip-а из Twitter Bootstrap
Исходный код директивы | Исходный код демо | Демо
angular.module("ExperimentsModule", [])
.directive("tbTooltip", function(){
return function(scope, element, iAttrs) {
iAttrs.$observe('title', function(value) {
element.removeData('tooltip');
element.tooltip();
});
}
});
Использоваться будет примерно так:
<span class="label label-warning"
tb-tooltip
title="You should pay order before {{order.cancelDateTime | date:'dd.MM.yyyy HH:mm'}}"
>
{{order.cancelDateTime | date:'dd.MM.yyyy HH:mm'}}
</span>
В данном примере создается новый модуль ExperimentsModule
. У него нет зависимостей (пустой список зависимостей) — никакие модули не должны быть загружены до него. В этом модуле создается директива tbTooltip
. Директивы при создании всегда именуются с использованием lowerCamelCase. При использовании директиву необходимо именовать в нижнем регистре с использованием в качестве разделителя одного из спец символов: :
, -
, или _
. По желанию для получения валидного кода можно использовать префиксы x-
или data-
. Примеры: tb:tooltip
, tb-tooltip
, tb_tooltip
, x-tb-tooltip
и data-tb-tooltip
.
За названием директивы идет фабричная функция, которая должна вернуть описание директивы. В общем случае описание представляет собой объект, полный список полей которого приведен в документации. Но существует упрощенный вариант, когда можно вернуть только postLink функцию. В этому случае директива в дальнейшем может использоваться только как атрибут какого-либо HTML элемента. В этом примере как раз использован упрощенный вариант создания директивы.
Что такое postLink функция? Когда директива выполняется для конкретного DOM элемента, ее работа состоит из 3-х фаз:
- compile: фаза, во время которой можно производить трансформацию шаблонной DOM-структуры элемента, к которому применяется директива. Под шаблонной структурой подразумевается либо внутренняя структура, описанная в самом коде HTML страницы, либо шаблон, заданный полями template или templateUrl конфигурационного объекта. Следующим примером будет как раз директива на базе compile функции;
- preLink: фаза, выполняемая перед связыванием всех дочерних элементов. Здесь не рекомендуется проводить какие-либо трансформации DOM;
- postLink: фаза, выполняемая после связывания всех дочерних элементов. Наиболее часто используемая фаза. Здесь рекомендуется выполнять все необходимые DOM трансформации, навешивать обработчики событий и т.п.
Последовательность выполнения фаз для иерархической структуры наглядно показана здесь.
В данном примере осталось еще два ключевых момента.
- В процессе использования необходимо, чтобы в атрибуте title мог быть не только статический текст, но и чтобы в нем поддерживалась интерполяция (подстановка) данных. Именно за это и ответственен код
iAttrs.$observe('title', function(value) { ... })
Как только интерполяция закончена, т.е. получена окончательная текстовая строка, или когда какие-либо данные, участвующие в интерполяции изменились, применяем изменения, используя tooltip компонент Twitter Bootstrap. - Второй момент, наверное, все же не очень ключевой и не имеет отношения к AngularJS, а касается логики работы компонента tooltip. Чтобы применить изменения для ранее созданного tooltip-а, надо подчистить старые данные. Что и делается кодом
element.removeData('tooltip');
Директива для подсветки кода
Исходный код директивы | Исходный код демо | Демо
.directive('uiSource', function () {
return {
restrict: 'EA',
compile: function (elem) {
var escape = function(content) {
return content
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
};
var pre = angular.element('<pre class="prettyprint linenums"></pre>');
pre.append(prettyPrintOne(escape(elem.html().slice(1)), undefined, true));
elem.replaceWith(pre);
}
};
});
Использование:
<ui-source>
<ui-source>
<div>
<label>Name:</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<hr>
<h1>Hello {{yourName}}!</h1>
</div>
</ui-source>
</ui-source>
Директива для подсветки кода с использованием google-code-prettify.
Необходимо, чтобы внутреннее содержимое этой директивы не компилировалось и не линковалось, а просто было обработано google-code-prettify.
Данная директива уже реализована через конфигурационный объект. Рассмотрим директиву построчно.
restrict: 'EA',
Директива может использоваться как элемент и как атрибут. В общем случае варианты применения кодируются как 'EACM'. Можно создать директиву, которая может использоваться как элемент 'E', атрибут 'A', класс 'C', комментарий 'M'.
terminal: true,
Означает, что приоритет на котором объявлена эта директива будет последним приоритетом исполнения. Т.е. будут выполнены только директивы приоритетом выше и с таким же. С таким же приоритетом будут выполнены все директивы, т.к. в рамках одного приоритета порядок исполнения директив не определен.
compile: function (elem) {
...
}
На этапе компиляции мы извлекаем содержимое элемента, обрабатываем спецсимволы, заменяя их на мнемоники, результат обрабатываем google-code-prettify, обрамляем это все тегом pre и заменяем исходный элемент получившимся.
Вот еще интересные варианты директив, задействующих этап компиляции: ng-if, transclude into an attribute. Оставляйте еще примеры в комментариях, добавлю в пост.
uiPagination
Исходный код директивы | Исходный код демо | Демо
Код достаточно длинный, поэтому сюда вставлять не буду.
Использование:
<ui-pagination cur="pagination.cur" total="pagination.total" display="9"></ui-pagination>
Классическая директива с визуальным компонентом.
Ключевая особенность здесь — использование изолированной области видимости (scope).
scope: {
cur: '=',
total: '=',
display: '@'
},
Статья уже получается достаточно большой, поэтому я не буду подробно останавливаться на деталях и всех возможных вариантах. Они хорошо описаны в документации. Кроме того, рекомендую ознакомиться со статьей The Nuances of Scope Prototypal Inheritance (там хорошие визуализации).
В данном случае cur и total будут двунаправлено привязаны через одноименные атрибуты к области видимости, в которой используется директива, а display будет получать обновления через одноименный атрибут из той же области видимости.
Единственное, что хотелось бы отметить: если создается директива с активным использованием NgModelController, то, скорее всего, лучше будет использовать не изолированную область видимости (с ней есть определенные проблемы), а новую дочернюю область видимости, объявляемую через scope: true
. Правда при этом в ng-model надо будет указывать свойство объекта (ng-model="pagination.cur"
), а не просто переменную (ng-model="curPage"
). Но просто переменные и не рекомендуется использовать для ng-model (пруф, смотрите комментарий Miško Hevery).
uiGrid
Исходный код директивы | Исходный код демо | Демо
Честно говоря, я все откладывал написание этой статьи, пока не напишу подобную директиву :-) Написал статью, смотрю, а она тут уже особо ничего не решает. Но раз уж написана, пусть будет как proof of concept. Можно, конечно, все настройки и через большой объект в атрибуте передавать как в ng-grid, но AngularJS может «круче», более декларативно. Поэтому подобный подход, мне кажется, более в духе AngularJS.
Использование:
$scope.data = [
{ column1: 'aaa', column2: '333', column3: 'aaa', column4: 'sdf' },
{ column1: 'bbb', column2: '222', column3: 'zzz', column4: 'sdf' },
{ column1: 'ccc', column2: '111', column3: 'ddd', column4: 'sdf' }
]
<ui-grid source="data">
<column name="column1"></column>
<column name="column2" sortable="true"></column>
<column name="column3" sortable="true"></column>
</ui-grid>
Ключевой момент здесь во взаимодействии директив через контроллер. Вот этот код require: '^uiGrid'
обеспечивает поиск необходимого контроллера на родительских элементах и передает его в link: function (scope, element, attrs, uiGridCtrl) { ... }
.
Заключение
Статья получилась немаленькая, но я в ней рассмотрел далеко не все. Читайте Developer Guide — он у них хороший и подробный. Вступайте в сообщество в Google+ — лавины постов там нет, но интересные моменты всплывают достаточно часто.
Автор: aav