Пользователь
0,0
рейтинг
1 января 2013 в 13:37

Разработка → Директивы в AngularJS

Директивы — это ключевая особенность 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 трансформации, навешивать обработчики событий и т.п.


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

В данном примере осталось еще два ключевых момента.
  1. В процессе использования необходимо, чтобы в атрибуте title мог быть не только статический текст, но и чтобы в нем поддерживалась интерполяция (подстановка) данных. Именно за это и ответственен код iAttrs.$observe('title', function(value) { ... }) Как только интерполяция закончена, т.е. получена окончательная текстовая строка, или когда какие-либо данные, участвующие в интерполяции изменились, применяем изменения, используя tooltip компонент Twitter Bootstrap.
  2. Второй момент, наверное, все же не очень ключевой и не имеет отношения к 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
карма
45,0
рейтинг 0,0

Самое читаемое Разработка

Комментарии (18)

  • +1
    Спасибо за хорошую статью. Developer Guide хорошо описывает но плохо организован.
    Жду пост про promises and deferreds в роутере.
    • 0
      Может кто-то и напишет. У меня есть некоторые сомнения, что я буду писать еще какие-то статьи в ближайшее время. Хотя все зависит от наличия свободного времени.

      В принципе, вот был хороший вопрос-ответ Misko Hevery. Ну и документация на $q и $routeProvider. Может кому-то эти ссылки помогут.
    • 0
      Перевод документации сойдет: http://www.angular.ru/api/ng.$q
  • 0
    Спасибо за статью. Подскажите, пожалуйста, как найти работу angularjs-разработчиком?
    • 0
      Интересный вопрос :-)

      Потока работы, конечно, пока нет. Хотя уже появляется понемногу на том же одеске. От одной даже не получилось два раза отказаться, на третий раз уже решил, что будет совсем глупо отказываться, раз так сильно денег хотят заплатить :-)

      Можно пробовать искать работу, где нет жестких требований по фронтэнд-фреймворку.

      Вот сейчас хотим попробовать функциональный прототип делать на AngularJS и моках RESTful-like API на javascript-данных. Потом на основе этого делать спецификацию RESTful-like интерфейса. Потом реализовывать его. Прикручивать его к уже реализованному прототипу. И так итеративно продвигаться вперед. AngularJS лаконичный, пока особых требований к дизайну нет — все должно быстро двигаться. Да и потом дизайн, за счет модульности, натянуть должно быть несложно.
      • 0
        Понятно. Большое спасибо за ответ!

        Хотел бы поработать с этой технологией, но, увы, никто пока работу не предлагает.
        • 0
          В смысле Вы никогда не работали с AngularJS, но хотели бы? И при этом только, когда работу предложат?
          Так придется долго ждать :)
          Понятно, что хорошо, когда работа учит, но все хорошо в меру.
          Наблюдая за собой и несколькими людьми, использующими AngularJS, могу сказать, что требуется время и иногда «волшебные пендали», чтобы мозги заработали в декларативном стиле. Императивный стиль всем более привычен. Но результат мне и тем, с кем я по AngularJS работал, нравится. Меньше кода. Выше продуктивность. Лучше читабельность. Легче поддержка.

          Опять же по своему опыту — немаленькую часть своего опыта и знаний по AngularJS я получил, следя за вопросами-ответами на stackoverflow и, в особенности, отвечая на них. Заодно и тамошней кармы немного поднабил :)
          • –1
            > И при этом только, когда работу предложат?

            Не люблю работать за бесплатно. Если работодателю нужен хороший программист, такой как я, то пусть оплачивает мое «обучение», я так считаю. Думаю, разобраться сходу в этом Ангулар не так уж сложно. Я почитал немного доки, там все вполне продуманно.

            И у меня большой опыт в декларативном программировании, не меньше(а может и больше), чем в императивном.
            • 0
              За бесплатно тоже не люблю. Правда самообучение не считаю бесплатной работой. Но не суть. Почему-то только сейчас вспомнил: «Are you a JS ninja? Would you like to become superhero, fight evil, and change the way web apps are written? Awesome, because we are hiring
              • 0
                aav, спасибо за ссылку. Выглядит очень интересно. Может быть попробую к ним, но пока я не планировал работать в таких глобальных проектах, и в таких больших конторах как Гугл. Использовать Энгьюла-джэйэс и разрабатывать его — это все-таки две совершенно разные по сложности и объему задачи.

                Насчет декларативного фронтенд-программирования. Посмотрите вот это, может быть вам тоже будет интересно: elm-lang.org/. На меня проект произвел большое впечатление, хотя он пока еще в достаточно зачаточной стадии.
  • 0
    В uiGrid хотелось бы демо с сортировкой для кирилицы.
    • 0
      Только что опробовал с кирилицей. Вроде нормально все. Коммитить, пушить не стал. Все же репозиторий не только для русскоязычных разработчиков. А в чем должна быть проблема?
  • 0
    Очень понравилась идея uiGrid, но вот дерева в нём не хватает — не подскажите в каких направлениях копнуть?
    • 0
      Не очень понял вопрос. Как на AngularJS tree сделать? Вот здесь подобраны некоторые примеры: github.com/angular/angular.js/wiki/JSFiddle-Examples. У Andy Joslin на первый взгляд мне показался почище код.
  • 0
    Имелся в виду компонент TreeGrid, пример функционала или тут.
    Примеров с простыми TreeView нашёл множество, с таблицами (гридами) тоже есть, а вот с TreeGrid-ом пока беда… Самостоятельная же реализация в моём понимании пока какой-то очень обёмной предстаёт, так что если только что-то узко под задачу смогу набросать…
  • 0
    А посоветуете хорошую книгу по основам? С английским не очень хорошо, но в принципе техническую литературу читать могу. Может, что-то свежее вышло, а то кроме Фримена почитать особо нечего.
    • 0
      Меня вот эта книга полностью устроила: ng-book
      • 0
        Спасибо!

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