Хорошо написанная директива должна
- решать одну задачу
- легко расширяться
- не конфликтовать с другими директивами
Разберем каждый пункт на примере поля для ввода пароля (думаю, всем знакомо поле с глазиком)
<input ng-model="user.password" ng-minlength="6" form-password form-error="Не менее 6 символов">
Сколько директив используется?
На самом деле их 7
ng-model
ng-minlength
form-password
— заменяет инпут на свой шаблон и меняет тип поля (password/text) при нажатии на глазикform-error
— показывает тултип ошибки если пароль недостаточно длинныйform-error-tooltip
— показывает тултип с заданным текстом над элементомform-error-tooltip-template
— вспомогательная директива с шаблоном тултипа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/
Добавить комментарий