How to show form input errors using AngularJS UI Bootstrap tooltip?

webvitaly picture webvitaly · Jul 13, 2014 · Viewed 43.7k times · Source

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?

Answer

pixelbits picture pixelbits · Jul 16, 2014

Demo Fiddle

Validation Tooltip Directive

The validationTooltip is the main directive. It has the following responsibilities:

  1. Define the tool tip template through its transcluded contents
  2. Keep track of validation expressions so that they can be evaluated with each digest cycle.
  3. Expose a controller API for allowing valiationMessage directives to register themselves
  4. 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:

  1. $form: Bound to the form model defined in parent scope. i.e. $scope.myForm
  2. $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:

  • `$form.$valid`
  • `$form.$invalid`
  • `$form.$dirty`
  • `$form.$pristine`
  • `$form.$error.required` etc...

$field properties:

  • `$field.$valid`
  • `$field.$invalid`
  • `$field.$dirty`
  • `$field.$pristine`
  • `$field.$error.required` etc...

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

  1. Add a form with a name attribute
  2. Add one or more form fields - each with a name attribute and an ng-model directive.
  3. Declare a <validation-tooltip> element with a target attribute referring to the name of one of the form fields.
  4. Apply the validation-message directive to each message with an optional ng-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>