For example I have the form where I am showing form input errors.
I need to show red badge (with 'hover to show errors') near input label if there are some errors. If user will hover this red badge - he will see list of errors using AngularJS UI Bootstrap tooltip. I don't want to put list of errors into tooltip-html-unsafe attribute, because it is not convenient to edit and maintain.
This code is more declarative:
<validation-tooltip ng-show="appForm.number.$invalid && appForm.number.$dirty">
<ul>
<li ng-show="appForm.number.$error.required">this field is required</li>
<li ng-show="appForm.number.$error.number">should be number</li>
<li ng-show="appForm.number.$error.min">minimum - 5</li>
<li ng-show="appForm.number.$error.max">miximum - 20</li>
</ul>
</validation-tooltip>
than this code:
<span tooltip-html-unsafe="{{<ul><li>This field is required;</li><li>...</li></ul>}}">hover to show errors</span>
How can I write such validation-tooltip directive using AngularJS UI Bootstrap tooltip?
Or maybe can you suggest another approach to maintain validation error messages?
Validation Tooltip Directive
The validationTooltip is the main directive. It has the following responsibilities:
- Define the tool tip template through its transcluded contents
- Keep track of validation expressions so that they can be evaluated with each digest cycle.
- Expose a controller API for allowing valiationMessage directives to register themselves
- Provide a 'target' attribute on the directive to specify which form field the badge (and the associated tooltip) will be bound to
Additional Notes
The tooltip template uses the transclusion function from the link function to bind the template to the directive's scope. There are two properties that are within scope that the template can bind to:
- $form: Bound to the form model defined in parent scope. i.e. $scope.myForm
- $field: Bound to the form.name model in parent scope. i.e. $scope.myForm.myInput
This allows the template to bind to validation properties such as $valid, $invalid, $pristine, $dirty, and $error without referring to the form name or the input field's name directly. For example, all of the following expressions are valid binding expressions:
$form properties:
$field properties:
Directive Implementation
app.directive('validationTooltip', function ($timeout) {
return {
restrict: 'E',
transclude: true,
require: '^form',
scope: {},
template: '<span class="label label-danger span1" ng-show="errorCount > 0">hover to show err</span>',
controller: function ($scope) {
var expressions = [];
$scope.errorCount = 0;
this.$addExpression = function (expr) {
expressions.push(expr);
}
$scope.$watch(function () {
var count = 0;
angular.forEach(expressions, function (expr) {
if ($scope.$eval(expr)) {
++count;
}
});
return count;
}, function (newVal) {
$scope.errorCount = newVal;
});
},
link: function (scope, element, attr, formController, transcludeFn) {
scope.$form = formController;
transcludeFn(scope, function (clone) {
var badge = element.find('.label');
var tooltip = angular.element('<div class="validationMessageTemplate tooltip-danger" />');
tooltip.append(clone);
element.append(tooltip);
$timeout(function () {
scope.$field = formController[attr.target];
badge.tooltip({
placement: 'right',
html: true,
title: clone
});
});
});
}
}
});
Validation Message Directive
The validationMessage directive keeps track of the validation messages to display in the tooltip. It uses ng-if
to define the expression to evaluate. If there is no ng-if
found on the element, then the expression simply evaluates to true (always shown).
app.directive('validationMessage', function () {
return {
restrict: 'A',
priority: 1000,
require: '^validationTooltip',
link: function (scope, element, attr, ctrl) {
ctrl.$addExpression(attr.ngIf || true );
}
}
});
Usage in HTML
- Add a form with a name attribute
- Add one or more form fields - each with a name attribute and an ng-model directive.
- Declare a
<validation-tooltip>
element with atarget
attribute referring to the name of one of the form fields.- Apply the
validation-message
directive to each message with an optionalng-if
binding expression.
<div ng-class="{'form-group': true, 'has-error':form.number.$invalid}">
<div class="row">
<div class="col-md-4">
<label for="number">Number</label>
<validation-tooltip target="number">
<ul class="list-unstyled">
<li validation-message ng-if="$field.$error.required">this field is required </li>
<li validation-message ng-if="$field.$error.number">should be number</li>
<li validation-message ng-if="$field.$error.min">minimum - 5</li>
<li validation-message ng-if="$field.$error.max">miximum - 20</li>
</ul>
</validation-tooltip>
</div>
</div>
<div class="row">
<div class="col-md-4">
<input type="number" min="5" max="20" ng-model="number" name="number" class="form-control" required />
</div>
</div>
</div>