Расширение и компоновка директив

от автора

Про директивы много написано, но мало о том как писать их правильно. Поделюсь опытом.

Хорошо написанная директива должна

  • решать одну задачу
  • легко расширяться
  • не конфликтовать с другими директивами

Разберем каждый пункт на примере поля для ввода пароля (думаю, всем знакомо поле с глазиком)

image

<input ng-model="user.password"        ng-minlength="6"        form-password        form-error="Не менее 6 символов"> 

Сколько директив используется?
На самом деле их 7

  1. ng-model
  2. ng-minlength
  3. form-password — заменяет инпут на свой шаблон и меняет тип поля (password/text) при нажатии на глазик
  4. form-error — показывает тултип ошибки если пароль недостаточно длинный
  5. form-error-tooltip — показывает тултип с заданным текстом над элементом
  6. form-error-tooltip-template — вспомогательная директива с шаблоном тултипа
  7. tooltip — директива из библиотеки Angular Bootstrap, которую мы расширим

Переопределение

Рассмотрим tooltip из angular-ui.github.io/bootstrap/#/tooltip и его исходный код

Первое, что бросается в глаза, это объект определения директивы, вынесенный в отдельный сервис $tooltip. Из-за чего директива определяется так:

.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {   return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); }]) 

Второе — отсутствие шаблона тултипа. Вместо него мы видим элемент, на котором создается и настраивается директива с именем directiveName +'-popup ' (directiveName = tooltip в этом случае):

'<div '+ directiveName +'-popup '+           'title="'+startSym+'tt_title'+endSym+'" '+           'content="'+startSym+'tt_content'+endSym+'" '+           'placement="'+startSym+'tt_placement'+endSym+'" '+           'animation="tt_animation" '+           'is-open="tt_isOpen"'+           '>'+  '</div>'; 

Найдем ниже эту директиву (назовем ее шаблонной):

.directive( 'tooltipPopup', function () {   return {     restrict: 'EA',     replace: true,     scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },     templateUrl: 'template/tooltip/tooltip-popup.html'   }; }) 

По адресу template/tooltip/tooltip-popup.html наконец-то найдем шаблон тултипа.

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

Сделаем директиву для вывода подсказок об ошибках со своим шаблоном. Подсказка будет появляться, если на элементе стриггериться событие open и исчезать, если — close

app.directive('formErrorTooltip', function($tooltip) {     return $tooltip('formErrorTooltip', 'formErrorTooltip', 'open'); }); 

Одна строчка. Круто?

В качестве шаблонной будет взята директива с именем form-error-tooltip-popup (помните directiveName +’-popup ‘). Создадим ее заранее, скопировав tooltip-popup, заменив имя и путь до шаблона.

События open и close не поддерживаются, но это не беда. Разработчики сделали возможность добавлять свои события.

app.config(function($tooltipProvider) {     $tooltipProvider.setTriggers({'open': 'close'}); }); 

Расширение

Итак, у нас есть form-error-tooltip, показывающая тултип ошибки над элементом. Сделаем так, чтобы она показывала ошибки, если введено мало символов (в настоящем проекте не сложно научить обрабатывать любые ошибки, в том числе серверные). Напишем директиву form-error, которая расширит первую:

app.directive('formError', function($compile, $interpolate, $injector) {     return {         terminal: true,         priority: 100,         scope: true,         compile: function compile(tElement, tAttrs) {             var startSym = $interpolate.startSymbol(),                 endSym = $interpolate.endSymbol(),                 ddo = {}, self = this;              tElement.attr('form-error-tooltip', startSym + 'message' + endSym);              angular.forEach(tAttrs.$attr, function(value, attr) {                 if ($injector.has(attr + 'Directive')) {                     ddo = $injector.get(attr + 'Directive')[0];                 }                 if (ddo.terminal && ddo.priority >= self.priority) {                     tElement.removeAttr(value);                 }             });              return function(scope, element, attrs, controller) {                 $compile(element)(scope);                                  var ngModelCtrl = element.controller('ngModel')                  element.on('input blur change', checkValidity);                  function checkValidity() {                     if (ngModelCtrl.$error.minlength) {                         setError(attrs.formError);                      } else if (element.scope() && element.scope().tt_isOpen) { //avoid tooltip bug                         element.triggerHandler('close');                     }                 }                  function setError(message) {                     scope.message = message;                     scope.$digest(); //set message to tooltip                     element.triggerHandler('open');                 }             };         }     }; }); 

Как видно из кода, она добавляет к элементу директиву form-error-tooltip и триггерит на нем события open и close в зависимости от того, есть ошибка или нет. Но как она это делает? Все дело в приоритетах.

Из-за установленных terminal: true, priority: 100 никакие другие не терминальные директивы или директивы с меньшим приоритетом не выполнятся. Другими словами до этой директивы выполнятся только ng-if, ng-repeat, ng-include и т.п., т.е. директивы, определяющие, нужно ли, вообще, показывать элемент.

Далее добавляем атрибут form-error-tooltip={{message}}, удаляем атрибуты директив с большим приоритетом (которые уже выполнились), в том числе саму директиву, чтобы она не выполняла себя бесконечно, и перекомпилируем элемент. Теперь выполнятся все оставшиеся директивы, в том числе директива показа тултипа, и не будет никаких конфликтов.

Компоновка

Похожим образом поступим с директивой пароля, которая должна заменить элемент на свой шаблон:

app.directive('formPassword', function($compile, $injector) {     return {         restrict: 'AE',         terminal: true,         priority: 200,         templateUrl: 'passwordTemplate.html',         replace: true,         scope: true,         compile: function(tElement, tAttrs) {             var input = tElement.find('input')[0], ddo = {}, self = this;              angular.forEach(tAttrs.$attr, function(value, attr) {                 if ($injector.has(attr + 'Directive')) {                     ddo = $injector.get(attr + 'Directive')[0];                 }                 if (attr !== 'type' && attr !== 'class' && attr !== 'formPassword' && (!ddo.terminal || ddo.priority < self.priority)) {                     input.setAttribute(value, tAttrs[attr]);                 }                 if (attr !== 'class') {                     tElement.removeAttr(value);                 }             });              return function(scope) {                 scope.show = false;             }         }     }; }); 

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

Приоритет у этой директивы больше чем у предыдущей, значит она создаст поле с глазиком и только после этого к нему применится директива обработки ошибок. С другой стороны, приоритет меньше чем у ng-if или ng-repeat, поэтому предварительно вся эта конструкция будет скрыта, показана или размножена сколько угодно раз и все будут дружить друг с другом.

Живой пример: plnkr.co/edit/BSVN7zZb0vNEAXo7iWhM?p=preview

ссылка на оригинал статьи http://habrahabr.ru/post/218385/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *