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
, которая может вызывать функцию, когда пользователь прокручивает элемент. Кроме этого, давайте также обрабатывать при этом значение смещения прокрутки.
<textarea scroll="onScroll(offset)">...</textarea>
Аналогично кнопке переключателю, мы передаем любую функцию, указанную в атрибуте 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 все становиться гораздо проще, если вы сосредоточите свои усилия на данных и состоянии, вместо событий.
Дополнительная информация
Директивы могут делать на много больше. Но все эти дополнительные возможности не охватываются в этой статье. Пожалуйста, посетите страницу документации по директивам для получения дополнительной информации.
Автор: Tulov_Alex