0,0
рейтинг
2 ноября 2013 в 09:40

Разработка → Разработка директив angularjs — это просто перевод tutorial

AngularJS директивы – это клево


AngularJS является каркасом (фреймворком) для построения web приложений, который позволяет создавать сложные приложения достаточно просто. Одна из его лучших возможностей, это создание директив, которые являются повторно используемыми web компонентами. Это дает возможность создавать новые HTML теги и атрибуты, которые могут динамично отображать контент в ответ на изменение данных, и обновлять сами данные, в случае необходимости.
Это очень высокопроизводительный подход, поскольку он позволяет вам оборачивать сложное взаимодействие с DOM в повторно используемые пакеты кода.

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


Пройдет немного времени, и вы поймете, насколько полезны директивы. Встроенные в AngularJS директивы являются прекрасным примером их разработки. Но в первое время, при создании директив возможны некоторые трудности с пониманием их работы. Команда Angular сделала хорошую работу, создав директивы чрезвычайно гибкими и мощными, хотя вся эта мощь дается начинающему не без труда.
В частности, трудно понять, как создать директиву, которая бы реагировала на изменение данных, изменяла данные, реагировала на определенные события или сама их возбуждала. В основном это сводится к одному вопросу:
Как мне взаимодействовать с директивой?
Эта статья призвана объяснить и упростить некоторые из наиболее распространенных проблем, которые могут возникать при создании директив.

Принципы создания директив


Директивы сделают вашу жизнь легче только в случае, если вы сможете их использовать без необходимости читать и изменять исходный код. В этом случае можно забыть как они работают, а просто знать что они делают.
Если вы ранее использовали один из ориентированных на представления фрэймворков, таких как Backbone, возможно вы захотите разделить ваше приложение на мелкие куски, используя директивы. Например, если вы хотите отобразить список пользователей, можно создать директиву, которая будет читать $scope.users и выводить их в представлении:
<user-list/>

Директива user-list работает. Заметьте, ее использование соответствует принципу «не повторяйся» (DRY)! Однако, давайте сравним ее с ng-repeat, которая обрабатывает только повторения. Какую из них можно использовать повторно в разных местах? Что делать, если вам нужно отобразить пользователей по разному в двух местах?
Хорошая директива делает что-то одно
ng-repeat лучше чем user-list потому что она отвечает только за одно действие: Она только повторяет определенную часть, так что ее можно использовать во многих ситуациях. Легко понять, что она делает. Вместо того, чтобы делать одну директиву, которая отвечает за все, лучше разбейте ее на несколько директив, которые будут выполнять специфические задачи, и используйте их вместе.
Хорошая директива не зависит от специфики приложения
Директивы тем более полезны, чем меньше они делают предположений относительно приложения. Директива, которая позволяет пользователю указать, за каким свойством наблюдать, такая как ng-model, является более полезной, чем директива, которая предполагает, что $scope.users существует. Как правило, если ваша директива должна использоваться в различных приложениях, она должна следовать этому правилу, чтобы можно было сказать, что она хорошо разработана, даже если вы и не собираетесь ее публиковать.
На сегодня достаточно теории. Давайте погрузимся в некоторые конкретные примеры, демонстрирующие распространенные способы взаимодействия с директивами.

Как отображать привязки


Первое, что нужно знать, как сделать директиву, которая будет отображать значение привязанного свойства: так как это делается с двойными фигурными скобками. Для примера давайте сделаем директиву, которая будет отображать фотографию и подпись к ней.
Первым шагом в проектировании любой директивы является выбор имен для атрибутов, которые будут представлять ее в вашем интерфейсе. Я выбрал photo-src для указания источника изображения, и caption для подписи. Будьте осторожны, чтобы не использовать имена, уже использующиеся другими директивами, такие как ng-src, если вы не знаете, как они работают.
Во вторых, нужно решить, будете ли вы поддерживать только атрибуты и имена классов, или также будете поддерживать элементы. В нашем случае, мы хотим чтобы photo был элементом.
<photo photo-src="{{photo.url}}"  caption="Taken on: {{photo.date}}"/>

Обратите внимание, что я не передавал директиве объект фотографии полностью. Такое решение позволяет лучше адаптировать директиву для работы с другой структурой данных.
Чтобы прочитать значения привязанных свойств, используется attrs.$observe. В этом случае функция обратного вызова будет вызываться каждый раз, когда значение привязанного свойства будет изменено. Затем мы используем element для внесения изменений в DOM.
app.directive('photo', function() {
    return {
        // обязательно, для поддержки работы через элемент
        restrict: 'E',

        // заменить <photo> этим html
        template: '<figure><img/><figcaption/></figure>',
        replace: true,

        // наблюдение и манипулирование DOM
        link: function($scope, element, attrs) {
            attrs.$observe('caption', function(value) {
                element.find('figcaption').text(value)
            })

            // атрибуты именуются с применением «верблюжьей» нотации
            attrs.$observe('photoSrc', function(value) {
                element.find('img').attr('src', value)
            })
        }
    }
}
})

Кроме этого, если ваш компонент имеет собственный шаблон, вы можете делать все это в изолированной области видимости.
app.directive('photo', function() {
    return {
        restrict: 'E',
        templateUrl: 'photo.html',
        replace: true,
        // передача двух атрибутов из attrs в область видимости шаблона
        scope: {
            caption: '@',
            photoSrc: '@'
        }
    }
})

<!-- photo.html -->
<figure>
    <img ng-src="{{photoSrc}}"/>
    <figcaption>{{caption}}</figcaption>
</figure>


Как читать и записывать данные


Некоторые директивы также должны записывать данные, например ng-model.
Давайте сделаем директиву для кнопки переключателя. Эта директива будет автоматически устанавливать состояние переключателя, в зависимости от некоторого логического значения в области видимости, и при клике на кнопке, она будет менять состояние на противоположное.
При передаче данных подобным образом, вам не нужно использовать фигурные скобки, вы используете «выражения». Выражение – это код javascript, который будет исполнен в определенной области видимости. Выражения можно использовать во всех случаях, когда вам нужно записать данные, или когда в директиву передается объект или массив, вместо строки.
<!--здесь нет двойных фигурных скобок -->
<button toggle="preferences.showDetails">Show Details</button>

Сначала мы используем = в scope: чтобы сделать scope.toggle доступным в нашей директиве. Хотя это явно не указано нигде внутри директивы, при использовании этого синтаксиса scope.toggle читает и записывает свойство, которое пользователь указал в атрибуте.
app.directive('toggle', function() {
    return {
        scope: {
            toggle: '=',
        },
        link: function($scope, element, attrs) {

Затем мы используем scope.$watch, которая выполняет переданную ей функцию каждый раз, когда значение выражения изменяется. Мы будем добавлять или удалять css класс active, внутри обработчика, вызываемого при изменениях.
            $scope.$watch("toggle", function(value) {
                element.toggleClass('active', value) 
            })

В конце, давайте подпишемся на событие click, где будем обновлять область видимости. Нам нужно использовать scope.$apply каждый раз, когда изменения происходят вне контекста выполнения Angular.
            element.click(function() {
                $scope.$apply(function() {
                    $scope.toggle = !$scope.toggle
                })
            })
        }
    }
})

Рабочее демо

Как экпонировать события


В некоторых случаях требуется, чтобы контроллер реагировал на события, происходящие внутри директивы, например как в ng-click. Давайте сделаем директиву scroll, которая может вызывать функцию, когда пользователь прокручивает элемент. Кроме этого, давайте также обрабатывать при этом значение смещения прокрутки.
...
Аналогично кнопке переключателю, мы передаем любую функцию, указанную в атрибуте scroll, в область видимости нашей директивы.
app.directive('scroll', function() {
    return {
        scope: {
            scroll: "&"
        },
        link: function($scope, element, attrs) {

Мы будем использовать событие прокрутки jQuery, чтобы получить нужное нам поведение. Здесь также нужно вызвать scope.$apply, потому что, хотя обработчик вызывается, он вызывается не в контексте контроллера.
            element.scroll(function() {
                $scope.apply(function() {
                    var offset = element.scrollTop()
                    $scope.scroll({offset:offset})
                })
            })
        }
    }
})

Обратите внимание, мы не передаем значение смещения в первом параметре, мы передаем хэш доступных параметров, и делаем их доступными внутри выражения onScroll(offset), которое было передано через атрибут. Это гораздо более гибкий подход, чем передача параметров напрямую, так как могут передаваться и другие параметры области видимости в соответствующие функции, например, текущий элемент в ng-repeat.
Рабочее демо

Как получить содержимое HTML


Директивы могут иметь любое содержимое html, но, как только вы зададите шаблон, их содержимое меняется на него.
Давайте создадим компонент modal: всплывающее окно с кнопкой закрытия, для которого требуется сохранить его содержимое, заданное в html.
<modal>
  <p>Some contents</p>
  <p>Put whatever you want in here</p>
</modal>

Наш элемент modal состоит более чем из одного элемента. Когда мы делаем шаблон, мы вставляем в него все полученное содержимое, там где это необходимо, просто добавив специальную директиву ng-transclude в div.
<div class="modal">
    <header>
        <button>Close</button>
        <h2>Modal</h2>
    </header>
    <div class="body" ng-transclude></div>
</div>

Передача содержимого из директивы шаблону включается очень просто. Чтобы это сделать, просто установите transclude: true:
app.directive('modal', function() {
    return {
        restrict: 'E',
        templateUrl: 'modal.html',
        replace: true,
        transclude: true,
    }
})

Для достижения более сложных результатов можно объединять любые методы из этой статьи.

Как реагировать на события


Иногда может потребовать вызвать функцию в вашей директиве при наступлении определенного события области видимости. Например, вы можете захотеть закрыть открытое модальное окно в случае нажатия пользователем клавиши escape.
Практически всегда это означает, что вы уделяете слишком много внимания событиям, хотя вы должны думать о потоке данных. Контроллеры не только содержат данные, они также содержат состояние представления. Нормальной практикой является иметь логическую переменную windowShown в контроллере, к которой привязываться с использованием ng-show, или передавать логическое значение в вашу директиву, способом, описанным выше.
Встречаются случаи, когда имеет смысл использовать $scope.$on в директиве, но для начала, вместо этого, постарайтесь подумать о проблеме с точки зрения изменения состояния. В Angular все становиться гораздо проще, если вы сосредоточите свои усилия на данных и состоянии, вместо событий.

Дополнительная информация


Директивы могут делать на много больше. Но все эти дополнительные возможности не охватываются в этой статье. Пожалуйста, посетите страницу документации по директивам для получения дополнительной информации.
Перевод: Sean Hess
Туловский Алексей @Tulov_Alex
карма
14,0
рейтинг 0,0
Програмист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

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

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

  • +5
    Перевод ужасен, форматирование убивает глаза, но как человек юзающий Ангулар — советую всем прочитать вдумчиво, ибо материал хороший.
  • 0
    Кстати текущая английская дока по директивам была недавно сущственно переписана: docs.angularjs.org/guide/directive и отличается от русского варианта.
  • 0
    Как получить содержимое HTML

    В разделе показано как передать html в шаблон директивы.
    А как получить содержимое HTML в директиву (для обработки)?
    • 0
      Функции link и compile вызываются с дом элементом в качестве второго и первого параметра.
      Кстати, буквально вчера разбирался, как сделать парсинг (аля трансформировать введенное значение в значение для модели) значения инпута с директивой ng-model, оказалось что все довольно просто, у контролера ng-model есть поля $paresers — трансформатор из инпута в модель и $formatters — из модели в инпут (фактически тот же фильтр) это обычные массивы. И, соответственно, все что нужно для того что бы достигнуть цели — создать директиву, указать require: 'ngModel', в линкер передастся ngModelCtrl (последним аргументом) и все что остается запушить функцию парсинга в переменную $parsers контролера ngModel. Может кому пригодится)
      • 0
        Это же костыли, я уверен что можно как-то сделать без ngModel.
        • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          ngModel не кастыль, просто он используется только для форм. Для обработки содержимого используйте compile/prelink. Хотя опять же не так часто это нужно. Если вам просто нужно обернуть содержимое чем-то, есть ngTransclude, если вам нужно провести какие-то манипуляции с разметкой (заэскейпить HTML, скажем для директивы подсветки синтаксиса) то используйте compile/link.
        • 0
          Почему костыль? Это вполне задокументированное решение. Все инпуты именно так и работают.
      • +2
        ваша заметка про ngModel не относится к обработке содержимого, не вводите людей в заблуждение. ngModel только для ввода данных, не для обработки содержимого.
        • 0
          Мне казалось, что это очевидно. Ответ на вопрос был в первом абзаце.
  • 0
    А можно оригинал статьи на английском?
  • 0
    Мне кажется аngular и слово просто — вещи несовместимые. Не зря первый комментарий по директивам, собравший 400+ лайков, говорит, что этот док не «дружелюбен» к пользователю, а второй, что это худший док, который был написан ever )) Да angular в целом дает много возможностей, но какой ценой. Приходит в голову такое сравнение, если jquery это школьная математика, то angular это высшая математика за второй или третий курс.
    • 0
      Это вы еще доки к three.js не читали.

      Там самые сложные вещи в полстроки расписано, и тебе повезло если там не стоит todo вместо текста.
      При этом такие прелестные детали WebGL, как то, что свойство repeat текстуры отрабатывает корректно только если длина текстуры степень двойки там опущены напрочь, и это ищешь почти день каждый раз.
    • +2
      Мне кажется аngular и слово просто — вещи несовместимые.

      Поэтому и появились такие фреймворки как CornerJS, Angular light и др.
      • +2
        А я думал эти библиотеки появлялись только потому, что идея директив всем нравится, но не все хотят таскать за собой весь ангулар.
        • +1
          Как человек, который запилил cornerJS — подтверждаю, думаете все правильно.
          С другой стороны — lega запилил Angular light, так что его точка зрения тоже верна :)
          • 0
            Только вот маленькое но: я предпочитаю использовать надежные в плане поддержки решения. Проще использовать angular со всей его избыточностью в маленьком проекте, нежели взять библиотеку, на поддержку которой разработчик может внезапно забить (всякое может случиться). Скажем Angular Light в bower нету, готовых к использованию билдов я не нашел, а это значит что после каждого обновления придется ее пересобирать. Ну в общем, проблемы небольших библиотек вполне известны. Так что я бы не стал экономить на килобайтах в просто взял решение с которым будем меньше всего проблем.
            • 0
              Дело вкуса, я лично все собираюсь добавить cornerJS в bower — некогда.
              Да и в моем случае билды всегда есть в репозитории, собранные через grunt, да и вся сборка — собрать вместе библиотеку и полифилл.

              Хотя это все субьективно, потому что разработка cornerJS завершена. Забивать тут не на что, он работает, ничего с ним не произойдет. Все тесты зеленые, тесты учитывают все сценарии. Он уже не будет изменяться, максимум — будут изменяться подключенные полифиллы (я сейчас хочу сменить простой WeakMap на сложный и хорошо работающий с памятью), может, пара дополнительных валидаций для имени директивы, но api и поведение библиотеки не будут изменяться.
              Последние две версии он полностью переписывался с сохранением формата для поддержки особенно сложных и необычных сценариев, и сейчас вроде бы они все учтены, вплоть до того что колбэки отрабатывают даже когда ты добавляешь класс к элементу — например замена class=«button directive-one» на class=«button directive-two» вызовет:
              1.unload для directive-one
              2.load для directive-two
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Я не заявил, что забросил, я заявил, что разработка завершена. Библиотека будет в дальнейшем оставаться в поддержке, но у нее не будет развития, я не вижу что в нее еще можно добавить, не покидая из концепцию микроархитектуры.
                  Возможно, на базе ее будет сделано что-либо еще, но сама она не будет расширяться, для нее не будут переименовываться опции, добавляться новые, удаляться устаревшие и так далее.

                  В любом случае, движок прямо базируется на MutationObserver(хотите — почитайте исходники, там все прозрачно до невозможности), который был утвержден в DOM4 working draft еще в 2012 году, и который полностью реализован — уже без префиксов — во всех браузерах: IE11, FF14+(боже, как давно это было) и chrome18 с префиксом, chrome27 без префикса(сами считайте, какой это webkit/blink). Для альтернативных реализаций вебкита(в сафари, опере, мобильных устройствах) — все тоже очень хорошо, его сейчас нет из последних версий браузеров только в родном браузере андроида.
                  О всех существующих смешных багах mutationObserver вроде работы с чекбоксами или не отрабатывающих колбэках в случае многократного изменения html прямо в его же колбэках я в курсе, успел наудивляться, пока разрабатывал.
                  Когда API находится в таком статусе — оно уже не может измениться, это вам не flexbox.
                  Конечно, я отслеживаю изменения, но это уже вопрос очистки совести.
  • +4
    Для тех, кто читает мануалы также невнимательно как и я, есть маленькая хитрость при объявлении шаблона.
    Кроме вышеупомянутых строки
    template: '<div><i>Something</i></div>'
    

    и файла-шаблона
    template: 'partial.html' 
    

    есть возможность использовать функцию, в которую будут переданы сам элемент и его аттрибуты
    <my-directive param1=value1 param2=value2 />
    
    ...
    
     template:   function(element, attrs) {
          if (attrs.param1 == value1) {
              return '<div><one-element></div>';
          } else {
              return '<div><other-element></div>';
          }
     }
    


    т.е. весьма удобно делать динамические шаблоны в зависимости от параметров, не прибегая к стандартным преобразованиям на этапе $compile, $link.
  • 0
    Только-только начал изучать angularjs, еще не привязал jquery, не смог повторить примеры у себя на локали, но чуть изучив материал, хочу немного дополнить для новичков:

    1. сначала что б привязывать директивы, необходимо инициализировать модуль:
    var app = angular.module('app', []);

    2. вместо «element.click(function() {» нужно использовать element.on(«click», function() {

    После этого все заработало. welcome!
    • 0
      element.bind вы хотели сказать? вы же не подключали jQuery.
      упс, как-то упустил из виду что есть уже и on и off
  • 0
    вот тут есть все методы работы с элементом docs.angularjs.org/api/angular.element

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