- PVSM.RU - https://www.pvsm.ru -

Директивы в AngularJS

Директивы — это ключевая особенность AngularJS [1]. С помощью директив можно добавлять новое поведение существующим HTML элементам, можно создавать новые компоненты. Примерами директив, добавляющих новое поведения для существующих HTML элементов, могут служить input [2], select [3], textarea [4] в связке с ngModel [5], required и т.п. Перечисленные директивы в основном связаны с валидацией форм [6] в AngularJS. Но тема валидации заслуживает отдельной статьи.

Директивы можно и нужно использовать для повышения модульности вашего приложения, выделения обособленной функциональности в компоненты, в том числе и для повторного использования.

Если вы разрабатываете приложение на AngularJS и не создаете директивы, то это уже само по себе немного настораживает. Либо ваше приложение достаточно простое и уложилось в стандартные возможности AngularJS, либо, скорее всего, что-то не так с архитектурой вашего приложения. А если у вас при этом есть работа с DOM-ом в контроллерах или сервисах, то вам однозначно надо разбираться с темой создания директив, т.к. манипуляций с DOM-ом не должно быть нигде, кроме директив.

В данной статье я постараюсь рассмотреть процесс создания собственных директив на нескольких примерах.

Хорошим примером создания директив могут служить репозитарии команды AngularUI [7]. В эту команду входят разработчики, не являющиеся сотрудниками Google, но очень хорошо зарекомендовавшие себя в списке рассылки [8] и на stackoverflow [9]. Насколько я могу судить, они создают production-ready компоненты с настройками, покрывающими большую часть вариантов использования. У меня тоже есть репозиторий [10], в который я выкладываю некоторые свои наработки. Но у меня немного другой подход. Мне больше нравится делать директивы под конкретные варианты использования. AngularJS очень лаконичен. Меньше кода => лучше читаемость => проще поддержка и изменение. Зачем тогда создавать «монструозные» компоненты с кучей настроек? Поэтому рассматривайте эти директивы как отправную точку для создания своих собственных под конкретные нужды. Еще за примерами можно пойти на сайт ngmodules.org [11], возможно, он сможет стать каталогом различных компонентов для AngularJS.

Итак, базовым документом для разработки своих директив является статья Directives [12] из Developer Guide [13]. Там все расписано очень хорошо и подробно. К этому документу придется возвращаться еще не раз.

Директива-обертка для Tooltip-а из Twitter Bootstrap

Исходный код директивы [14] | Исходный код демо [15] | Демо [16]

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>

В данном примере создается новый модуль [17] 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 трансформации, навешивать обработчики событий и т.п.

Последовательность выполнения фаз для иерархической структуры наглядно показана здесь [18].

В данном примере осталось еще два ключевых момента.

  1. В процессе использования необходимо, чтобы в атрибуте title мог быть не только статический текст, но и чтобы в нем поддерживалась интерполяция (подстановка) данных. Именно за это и ответственен код iAttrs.$observe('title', function(value) { ... }) Как только интерполяция закончена, т.е. получена окончательная текстовая строка, или когда какие-либо данные, участвующие в интерполяции изменились, применяем изменения, используя tooltip компонент Twitter Bootstrap.
  2. Второй момент, наверное, все же не очень ключевой и не имеет отношения к AngularJS, а касается логики работы компонента tooltip. Чтобы применить изменения для ранее созданного tooltip-а, надо подчистить старые данные. Что и делается кодом element.removeData('tooltip');

Директива для подсветки кода

Исходный код директивы [19] | Исходный код демо [20] | Демо [21]

.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) {
            ...
        }

На этапе компиляции мы извлекаем содержимое элемента, обрабатываем спецсимволы, заменяя их на мнемоники [22], результат обрабатываем google-code-prettify, обрамляем это все тегом pre и заменяем исходный элемент получившимся.

Вот еще интересные варианты директив, задействующих этап компиляции: ng-if [23], transclude into an attribute [24]. Оставляйте еще примеры в комментариях, добавлю в пост.

uiPagination

Исходный код директивы [25] | Исходный код демо [26] | Демо [27]

Код достаточно длинный, поэтому сюда вставлять не буду.

Использование:

    <ui-pagination cur="pagination.cur" total="pagination.total" display="9"></ui-pagination>

Классическая директива с визуальным компонентом.

Ключевая особенность здесь — использование изолированной области видимости (scope).

scope: {
    cur: '=',
    total: '=',
    display: '@'
},

Статья уже получается достаточно большой, поэтому я не буду подробно останавливаться на деталях и всех возможных вариантах. Они хорошо описаны в документации. Кроме того, рекомендую ознакомиться со статьей The Nuances of Scope Prototypal Inheritance [28] (там хорошие визуализации).

В данном случае cur и total будут двунаправлено привязаны через одноименные атрибуты к области видимости, в которой используется директива, а display будет получать обновления через одноименный атрибут из той же области видимости.

Единственное, что хотелось бы отметить: если создается директива с активным использованием NgModelController [29], то, скорее всего, лучше будет использовать не изолированную область видимости (с ней есть определенные проблемы [30]), а новую дочернюю область видимости, объявляемую через scope: true. Правда при этом в ng-model надо будет указывать свойство объекта (ng-model="pagination.cur"), а не просто переменную (ng-model="curPage"). Но просто переменные и не рекомендуется использовать для ng-model (пруф [31], смотрите комментарий Miško Hevery).

uiGrid

Исходный код директивы [32] | Исходный код демо [33] | Демо [34]

Честно говоря, я все откладывал написание этой статьи, пока не напишу подобную директиву :-) Написал статью, смотрю, а она тут уже особо ничего не решает. Но раз уж написана, пусть будет как proof of concept. Можно, конечно, все настройки и через большой объект в атрибуте передавать как в ng-grid [35], но 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 [36] — он у них хороший и подробный. Вступайте в сообщество в Google+ [37] — лавины постов там нет, но интересные моменты всплывают достаточно часто.

Автор: aav

Источник [38]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/23917

Ссылки в тексте:

[1] AngularJS: http://angularjs.org/

[2] input: http://docs.angularjs.org/api/ng.directive:input

[3] select: http://docs.angularjs.org/api/ng.directive:select

[4] textarea: http://docs.angularjs.org/api/ng.directive:textarea

[5] ngModel: http://docs.angularjs.org/api/ng.directive:ngModel

[6] валидацией форм: http://docs.angularjs.org/guide/forms

[7] репозитарии команды AngularUI: https://github.com/angular-ui

[8] списке рассылки: https://groups.google.com/forum/?fromgroups#!forum/angular

[9] stackoverflow: http://stackoverflow.com/questions/tagged/angularjs

[10] репозиторий: https://github.com/andreev-artem/angular_experiments

[11] ngmodules.org: http://ngmodules.org/

[12] Directives: http://docs.angularjs.org/guide/directive

[13] Developer Guide: http://docs.angularjs.org/guide/

[14] Исходный код директивы: https://github.com/andreev-artem/angular_experiments/blob/master/tb-tooltip/tb-tooltip.js

[15] Исходный код демо: https://github.com/andreev-artem/angular_experiments/blob/master/tb-tooltip/index.html

[16] Демо: http://andreev-artem.github.com/angular_experiments/tb-tooltip/index.html

[17] модуль: http://docs.angularjs.org/api/angular.module

[18] здесь: http://jsfiddle.net/vojtajina/8yzbZ/

[19] Исходный код директивы: https://github.com/andreev-artem/angular_experiments/blob/master/ui-source/ui-source.js

[20] Исходный код демо: https://github.com/andreev-artem/angular_experiments/blob/master/ui-source/index.html

[21] Демо: http://andreev-artem.github.com/angular_experiments/ui-source/index.html

[22] мнемоники: http://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%B5%D0%BC%D0%BE%D0%BD%D0%B8%D0%BA%D0%B8_%D0%B2_HTML

[23] ng-if: https://github.com/andreev-artem/angular_experiments/blob/master/ng-if/ng-if.js

[24] transclude into an attribute: http://stackoverflow.com/q/11703086/457375

[25] Исходный код директивы: https://github.com/andreev-artem/angular_experiments/blob/master/ui-pagination/ui-pagination.js

[26] Исходный код демо: https://github.com/andreev-artem/angular_experiments/blob/master/ui-pagination/index.html

[27] Демо: http://andreev-artem.github.com/angular_experiments/ui-pagination/index.html

[28] The Nuances of Scope Prototypal Inheritance: https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance

[29] NgModelController: http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController

[30] определенные проблемы: http://stackoverflow.com/q/11896732/457375

[31] пруф: https://plus.google.com/118090665492423851447/posts/KKiLKLCF4Xa

[32] Исходный код директивы: https://github.com/andreev-artem/angular_experiments/blob/master/ui-grid/ui-grid.js

[33] Исходный код демо: https://github.com/andreev-artem/angular_experiments/blob/master/ui-grid/index.html

[34] Демо: http://andreev-artem.github.com/angular_experiments/ui-grid/index.html

[35] ng-grid: https://github.com/angular-ui/ng-grid

[36] Developer Guide: http://docs.angularjs.org/guide

[37] сообщество в Google+: https://plus.google.com/communities/115368820700870330756

[38] Источник: http://habrahabr.ru/post/164493/