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

44,818

Solution 1

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>

Solution 2

@pixelbits answer is great. I used this instead:

  <div class="form-group" ng-class="{ 'has-error': form.name.$dirty && form.name.$invalid }">
    <label for="name" class="col-sm-4 control-label">What's your name?</label>
    <div class="col-sm-6">
      <input class="form-control has-feedback" id="name" name="name" 
        required
        ng-minlength="4"
        ng-model="formData.name"
        tooltip="{{form.name.$valid ? '' : 'How clients see your name.  Min 4 chars.'}}"  tooltip-trigger="focus" 
        tooltip-placement="below">
      <span class="glyphicon glyphicon-ok-sign text-success form-control-feedback" aria-hidden="true"
        ng-show="form.name.$valid"></span>
    </div>
  </div>

The technique is ui-bootstrap's tooltip and set the tooltip text to '' when valid.

http://jsbin.com/ditekuvipa/2/edit

Solution 3

Great answer from @pixelbits. I used his directives and modified them slightly to allow the tooltip to display over the actual input as some users requested.

angular.module('app')
    .directive('validationTooltip', ['$timeout', function ($timeout) {

    function toggleTooltip(scope) {
        if (!scope.tooltipInstance) {
            return;
        }

        $timeout(function() {
            if (scope.errorCount > 0 && (scope.showWhen == undefined || scope.showWhen())) {
                scope.tooltipInstance.enable();
                scope.tooltipInstance.show();
            } else {
                scope.tooltipInstance.disable();
                scope.tooltipInstance.hide();
            }
        });
    }

    return {
        restrict: 'E',
        transclude: true,
        require: '^form',
        scope: {
            showWhen: '&',
            placement: '@',
        },
        template: '<div></div>',
        controller: ['$scope', 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;

                toggleTooltip($scope);
            });

        }],
        link: function (scope, element, attr, formController, transcludeFn) {
            scope.$form = formController;

            transcludeFn(scope, function (clone) {

                var tooltip = angular.element('<div class="validationMessageTemplate" style="display: none;"/>');
                tooltip.append(clone);
                element.append(tooltip);
                $timeout(function () {
                    scope.$field = formController[attr.target];

                    var targetElm = $('[name=' + attr.target + ']');
                    targetElm.tooltip({
                        placement: scope.placement != null ? scope.placement : 'bottom',
                        html: true,
                        title: clone,
                    });

                    scope.tooltipInstance = targetElm.data('bs.tooltip');
                    toggleTooltip(scope);

                    if (scope.showWhen) {
                        scope.$watch(scope.showWhen, function () {
                            toggleTooltip(scope);
                        });
                    }
                });
            });
        }
    }
}]);

The major change is that the directive uses jQuery to find the target element (which should be an input) via the name attribute, and initializes the tooltip on that element rather than the element of the directive. I also added a showWhen property to the scope as you may not always want your tooltip to show when the input is invalid (see example below).

The validationMessage directive is unchanged

angular.module('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 is also similar, with just the addition of showWhen if you want:

<div class="form-group" ng-class="{ 'has-error' : aForm.note.$invalid && (aForm.note.$dirty) }">
    <label class="col-md-3 control-label">Note</label>
    <div class="col-md-15">
        <textarea
            name="note"
            class="form-control"
            data-ng-model="foo.Note"
            ng-required="bar.NoteRequired"></textarea>
        <validation-tooltip target="note" show-when="aForm.note.$invalid && (aForm.note.$dirty)">
            <ul class="validation-list">
                <li validation-message ng-if="$field.$error.required">Note required</li>
            </ul>
        </validation-tooltip>
    </div>
</div>

Solution 4

you can actually just use the tooltip-enable property:

<div class="showtooltip" tooltip-placement="left" tooltip-enable="$isValid" tooltip="tooltip message"></div>

Solution 5

My goal was to leverage both ng-messages and ui-bootstrap popover for validation feedback. I prefer the popover vs. the tooltip as it displays the help-block styles more clearly.

Here's the code:

<!-- Routing Number -->
<div class="form-group-sm" ng-class="{ 'has-error' : form.routingNumber.$invalid && !form.routingNumber.$pristine }">
    <label class="control-label col-sm-4" for="routing-number">Routing #</label>
    <div class="col-sm-8">
        <input class="form-control input-sm text-box"
            id="routing-number"
            name="routingNumber"
            ng-model="entity.ROUTINGNUM"                        
            popover-class="help-block"
            popover-is-open="form.routingNumber.$invalid"
            popover-trigger="none"
            required
            uib-popover-template="'routing-number-validators'"
            string-to-number
            type="number" />
    </div>                
    <!-- Validators -->
    <script type="text/ng-template" id="routing-number-validators">
        <div ng-messages="form.routingNumber.$error">
            <div ng-messages-include="/app/modules/_core/views/validationMessages.html"></div>
        </div>
    </script>
</div>

Here is the validationMessages.html

<span ng-message="required">Required</span>
<span ng-message="max">Too high</span>
<span ng-message="min">Too low</span>
<span ng-message="minlength">Too short</span>
<span ng-message="maxlength">Too long</span>
<span ng-message="email">Invalid email</span>

Note: I had to upgrade to jQuery 2.1.4 to get the uib-popover-template directive to work.

Dependencies:

Share:
44,818

Related videos on Youtube

webvitaly
Author by

webvitaly

web-developer

Updated on July 09, 2022

Comments

  • webvitaly
    webvitaly almost 2 years

    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?

  • Alejandro Sanz Díaz
    Alejandro Sanz Díaz over 9 years
    I tried to use your directives but it returns me this: Controller 'form', required by directive 'validationTooltip', can't be found! What can it be?
  • pixelbits
    pixelbits over 9 years
    Check to see if your HTML is well formed and that you're not missing any closing tags. The validationTooltip and validationMessage directives must be within a former tag
  • Gayan
    Gayan over 9 years
    how about if we want to show the error tool tip to input field instead of label? is it possible?
  • felixfbecker
    felixfbecker almost 9 years
    This gets complicated quickly when you want to show a HTML list showing whats wrong with the input, for example security criteria for a password.
  • rjh
    rjh almost 8 years
    This is good, but it will only show the tooltip on focus, so e.g. if you have 5 letters and you delete 2, no tooltip will be displayed until you blur and focus the field again.