72

I am using Angular with Bootstrap. Here is the code for reference:

<form name="newUserForm" ng-submit="add()" class="" novalidate>
    <input type="text" class="input" ng-model="newUser.uname" placeholder="Twitter" ng-pattern="/^@[A-Za-z0-9_]{1,15}$/" required></td>
    <button type="submit" ng-disabled="newUserForm.$invalid" class="btn btn-add btn-primary">Add</button>
</form>

Bootstrap has styles for invalid fields in the form of input:invalid {.... }; these kick in when the field is empty. Now I also have some pattern matching via Angular. This creates odd cases when ":invalid" is off, but ".ng-invalid" is on, which would require me to re-implement bootstrap CSS classes for the ".ng-invalid" class.

I see two options, but having trouble with both

  • Make Angular use some custom classname instead of "ng-valid" (I don't know how to do this).
  • Disable html5 validation (I thought that that's what "novalidate" attribute in the form tag should do, but couldn't get it work for some reason).

The Angular-Bootstrap directives out there don't cover styling.

Ivan P
  • 1,920
  • 2
  • 15
  • 19
  • 3
    novalidate should "disable browser's native form validation" -- [form docs](http://docs.angularjs.org/guide/forms) – Mark Rajcok Jan 15 '13 at 23:03

12 Answers12

92

Use Bootstrap's "error" class for styling. You can write less code.

<form name="myForm">
  <div class="control-group" ng-class="{error: myForm.name.$invalid}">
    <label>Name</label>
    <input type="text" name="name" ng-model="project.name" required>
    <span ng-show="myForm.name.$error.required" class="help-inline">
        Required</span>
  </div>
</form>

EDIT: As other answers and comments point out - in Bootstrap 3 the class is now "has-error", not "error".

Ivan P
  • 1,920
  • 2
  • 15
  • 19
whuhacker
  • 1,064
  • 8
  • 7
  • 53
    Or if you are using bootstrap 3 `ng-class="{'has-error': myForm.name.$invalid}"` – Bibek Shrestha Nov 10 '13 at 12:32
  • 17
    You can also add `&& myForm.name.$dirty` to make the validation style only show after the user has interacted with the form control. – mWillis Feb 28 '14 at 18:09
  • @bibstha what about help-inline for bs3? – SuperUberDuper Oct 09 '14 at 20:21
  • 5
    This works but it's a massive overhead and makes templates incredibly verbose. I'm looking for a neater way. – BenCr Nov 14 '14 at 15:27
  • Which I found as soon as I scrolled down a couple of answers. – BenCr Nov 14 '14 at 15:29
  • I think this answer is out of date with bootstrap 4: https://getbootstrap.com/docs/4.0/migration/#forms-1. From the second to last bullet: "Replaced .has-error, .has-warning, and .has-success classes with HTML5 form validation via CSS’s :invalid and :valid pseudo-classes." Here's the rule I'm seeing: `.was-validated .form-control:invalid, .form-control.is-invalid, .was-validated .custom-select:invalid, .custom-select.is-invalid { border-color: #dc3545; }` – Sammaron Feb 21 '19 at 17:52
47

The classes have changed in Bootstrap 3:

<form class="form-horizontal" name="form" novalidate ng-submit="submit()" action="/login" method="post">
  <div class="row" ng-class="{'has-error': form.email.$invalid, 'has-success': !form.email.$invalid}">
    <label for="email" class="control-label">email:</label>
    <div class="col">
    <input type="email" id="email" placeholder="email" name="email" ng-model="email" required>
    <p class="help-block error" ng-show="form.email.$dirty && form.email.$error.required">please enter your email</p>
    <p class="help-block error" ng-show="form.email.$error.email">please enter a valid email</p>
  ...

Note the quotes around 'has-error' and 'has-success': took a while to find that...

malix
  • 3,566
  • 1
  • 31
  • 41
  • Has anyone gotten `ng-class="(form.email.$invalid ? 'has-error' : 'has-success')"` to work? – kristianlm Aug 08 '13 at 22:00
  • 4
    To avoid the inputs appearing invalid right after page-load I think you should check the $dirty property indicating whether the field was already edited: `{'has-error': form.email.$dirty && form.email.$invalid, 'has-success': form.email.$dirty && !form.email.$invalid}` But now this expression becomes so long that it gets prone to typing errors and hard to read and it is always similar so there should be a better way, not? – mono68 Sep 23 '13 at 22:36
  • 1
    I use a directive that adds a "submitted" flag to the form for that See: http://stackoverflow.com/questions/14965968/angularjs-browser-autofill-workaround-by-using-a-directive/19410229#19410229 – malix Oct 16 '13 at 17:49
  • 1
    @kristianlm did you tryng-class=" {'has-error': form.email.$invalid, 'has-success': !form.email.$invalid}" on a div ABOVE the input... – malix Oct 16 '13 at 17:53
  • @malix that might work, but I was looking to not have to repeat form.email.$invalid. – kristianlm Nov 05 '13 at 17:18
  • You need to put ' around the classname because it has a - in it. Classnames without - work also without ' :-)! It is because has-error would be interpreted as a subtraction operation with 2 strings which is not possible in javascript as a name of a property. – Sebastian Jul 31 '14 at 07:22
34

Another solution: Create directive which toggles has-error class according to a child input.

app.directive('bsHasError', [function() {
  return {
      restrict: "A",
      link: function(scope, element, attrs, ctrl) {
          var input = element.find('input[ng-model]'); 
          if (input.length) {
              scope.$watch(function() {
                  return input.hasClass('ng-invalid');
              }, function(isInvalid) {
                  element.toggleClass('has-error', isInvalid);
              });
          }
      }
  };
}]);

and then simple use it in template

<div class="form-group" bs-has-error>
    <input class="form-control" ng-model="foo" ng-pattern="/.../"/>
</div>
Tamlyn
  • 22,122
  • 12
  • 111
  • 127
farincz
  • 4,943
  • 1
  • 28
  • 38
  • 3
    I think a directive is the best solution in this case. – JKillian Feb 28 '14 at 21:57
  • 2
    Worth noting that jqlite doesn't support the `element[attribute]` selector syntax so this needs modifying a little if you're not using jQuery. – Tamlyn Apr 14 '15 at 15:57
  • 1
    The watch will never get disposed when not supplying an expression – Anders May 29 '15 at 09:11
  • 3
    Instead of looking at the class, would it not be better to look directly at the NgModelController attributes ? `input.controller('ngModel').$invalid` instead of `input.hasClass('ng-invalid')`. I had issue with the css class not being updated before the $watch triggers. – Thomas Wajs Jun 12 '15 at 15:24
  • @Thomas Wajs is absolutely correct. The above solution (As well as several of the others posted here) will always be one digest cycle out of sync because the evaluation is performed in the middle of the digest cycle before the invalid classes have been updated. input.controller('ngModel').$invalid fixes this problem. – RonnBlack Jan 09 '16 at 05:24
22

Minor improvement to @farincz's answer. I agree that a directive is the best approach here but I didn't want to have to repeat it on every .form-group element so I updated the code to allow adding it to either the .form-group or to the parent <form> element (which will add it to all contained .form-group elements):

angular.module('directives', [])
  .directive('showValidation', [function() {
    return {
        restrict: "A",
        link: function(scope, element, attrs, ctrl) {

            if (element.get(0).nodeName.toLowerCase() === 'form') {
                element.find('.form-group').each(function(i, formGroup) {
                    showValidation(angular.element(formGroup));
                });
            } else {
                showValidation(element);
            }

            function showValidation(formGroupEl) {
                var input = formGroupEl.find('input[ng-model],textarea[ng-model]');
                if (input.length > 0) {
                    scope.$watch(function() {
                        return input.hasClass('ng-invalid');
                    }, function(isInvalid) {
                        formGroupEl.toggleClass('has-error', isInvalid);
                    });
                }
            }
        }
    };
}]);
Community
  • 1
  • 1
emertechie
  • 3,607
  • 2
  • 22
  • 22
17

Minor improvement to @Andrew Smith's answer. I change input elements and using require keyword.

.directive('showValidation', [function() {
    return {
        restrict: "A",
        require:'form',
        link: function(scope, element, attrs, formCtrl) {
            element.find('.form-group').each(function() {
                var $formGroup=$(this);
                var $inputs = $formGroup.find('input[ng-model],textarea[ng-model],select[ng-model]');

                if ($inputs.length > 0) {
                    $inputs.each(function() {
                        var $input=$(this);
                        scope.$watch(function() {
                            return $input.hasClass('ng-invalid');
                        }, function(isInvalid) {
                            $formGroup.toggleClass('has-error', isInvalid);
                        });
                    });
                }
            });
        }
    };
}]);
Shashank Agrawal
  • 25,161
  • 11
  • 89
  • 121
Jason Im
  • 301
  • 2
  • 7
  • Needs to be changed to $(element). jqlite does not support search by class. (I tried to submit an edit but I need to change 6 characters....) – Ben George Nov 30 '16 at 03:33
11

Thank you to @farincz for a great answer. Here are some modifications I have made to fit with my use case.

This version provides three directives:

  • bs-has-success
  • bs-has-error
  • bs-has (a convenience for when you want to use the other two together)

Modifications I have made:

  • Added a check to only show the has states when the form field is dirty, i.e. they won't be shown until somebody interacts with them.
  • Altered the string passed into element.find() for those not using jQuery, as element.find() in Angular's jQLite only supports finding elements by tagname.
  • Added support for select boxes and textareas.
  • Wrapped the element.find() in a $timeout to support cases where the element may not yet have it's children rendered to the DOM (e.g. if a child of the element is marked with ng-if).
  • Changed if expression to check for the length of the returned array (if(input) from @farincz's answer always returns true, as the return from element.find() is a jQuery array).

I hope somebody finds this useful!

angular.module('bs-has', [])
  .factory('bsProcessValidator', function($timeout) {
    return function(scope, element, ngClass, bsClass) {
      $timeout(function() {
        var input = element.find('input');
        if(!input.length) { input = element.find('select'); }
        if(!input.length) { input = element.find('textarea'); }
        if (input.length) {
            scope.$watch(function() {
                return input.hasClass(ngClass) && input.hasClass('ng-dirty');
            }, function(isValid) {
                element.toggleClass(bsClass, isValid);
            });
        }
      });
    };
  })
  .directive('bsHasSuccess', function(bsProcessValidator) {
    return {
      restrict: 'A',
      link: function(scope, element) {
        bsProcessValidator(scope, element, 'ng-valid', 'has-success');
      }
    };
  })
  .directive('bsHasError', function(bsProcessValidator) {
    return {
      restrict: 'A',
      link: function(scope, element) {
        bsProcessValidator(scope, element, 'ng-invalid', 'has-error');
      }
    };
  })
  .directive('bsHas', function(bsProcessValidator) {
    return {
      restrict: 'A',
      link: function(scope, element) {
        bsProcessValidator(scope, element, 'ng-valid', 'has-success');
        bsProcessValidator(scope, element, 'ng-invalid', 'has-error');
      }
    };
  });

Usage:

<!-- Will show success and error states when form field is dirty -->
<div class="form-control" bs-has>
  <label for="text"></label>
  <input 
   type="text" 
   id="text" 
   name="text" 
   ng-model="data.text" 
   required>
</div>

<!-- Will show success state when select box is anything but the first (placeholder) option -->
<div class="form-control" bs-has-success>
  <label for="select"></label>
  <select 
   id="select" 
   name="select" 
   ng-model="data.select" 
   ng-options="option.name for option in data.selectOptions"
   required>
    <option value="">-- Make a Choice --</option>
  </select>
</div>

<!-- Will show error state when textarea is dirty and empty -->
<div class="form-control" bs-has-error>
  <label for="textarea"></label>
  <textarea 
   id="textarea" 
   name="textarea" 
   ng-model="data.textarea" 
   required></textarea>
</div>

You can also install Guilherme's bower package that bundles all this together.

Community
  • 1
  • 1
Tom Spencer
  • 7,816
  • 4
  • 54
  • 50
  • 1
    I've published it on bower as "angular-bootstrap-validation" with the credits to you and @farincz, hope you don't mind – Gui Jun 16 '14 at 11:49
  • 2
    That's cool. I notice you've added in a feature where you can put the directive at the form level and have it recurse through the nested `.form-group` elements. That's nice but it won't work unless you include jQuery, as the built-in angular jqlite implementation of `find` only supports finding by tagname, not by selector. You might want to add a note in the README to that effect. – Tom Spencer Jun 25 '14 at 15:19
  • You can use `form.$submitted` to only show errors on submit. There's an example in the Angular docs here: https://docs.angularjs.org/guide/forms. Look for the heading "Binding to form and control state". – Jeff Kilbride Apr 27 '16 at 08:25
4

If styling is the issue, but you don't want to disable the native validation, why not override the styling with your own, more specific style?

input.ng-invalid, input.ng-invalid:invalid {
   background: red;
   /*override any styling giving you fits here*/
}

Cascade your problems away with CSS selector specificity!

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
2

My improvement to Jason Im's answer the following adds two new directives show-validation-errors and show-validation-error.

'use strict';
(function() {

    function getParentFormName(element,$log) {
        var parentForm = element.parents('form:first');
        var parentFormName = parentForm.attr('name');

        if(!parentFormName){
            $log.error("Form name not specified!");
            return;
        }

        return parentFormName;
    }

    angular.module('directives').directive('showValidation', function () {
        return {
            restrict: 'A',
            require: 'form',
            link: function ($scope, element) {
                element.find('.form-group').each(function () {
                    var formGroup = $(this);
                    var inputs = formGroup.find('input[ng-model],textarea[ng-model],select[ng-model]');

                    if (inputs.length > 0) {
                        inputs.each(function () {
                            var input = $(this);
                            $scope.$watch(function () {
                                return input.hasClass('ng-invalid') && !input.hasClass('ng-pristine');
                            }, function (isInvalid) {
                                formGroup.toggleClass('has-error', isInvalid);
                            });
                            $scope.$watch(function () {
                                return input.hasClass('ng-valid') && !input.hasClass('ng-pristine');
                            }, function (isInvalid) {
                                formGroup.toggleClass('has-success', isInvalid);
                            });
                        });
                    }
                });
            }
        };
    });

    angular.module('directives').directive('showValidationErrors', function ($log) {
        return {
            restrict: 'A',
            link: function ($scope, element, attrs) {
                var parentFormName = getParentFormName(element,$log);
                var inputName = attrs['showValidationErrors'];
                element.addClass('ng-hide');

                if(!inputName){
                    $log.error("input name not specified!")
                    return;
                }

                $scope.$watch(function () {
                    return !($scope[parentFormName][inputName].$dirty && $scope[parentFormName][inputName].$invalid);
                },function(noErrors){
                    element.toggleClass('ng-hide',noErrors);
                });

            }
        };
    });

    angular.module('friport').directive('showValidationError', function ($log) {
        return {
            restrict: 'A',
            link: function ($scope, element, attrs) {
                var parentFormName = getParentFormName(element,$log);
                var parentContainer = element.parents('*[show-validation-errors]:first');
                var inputName = parentContainer.attr('show-validation-errors');
                var type = attrs['showValidationError'];

                element.addClass('ng-hide');

                if(!inputName){
                    $log.error("Could not find parent show-validation-errors!");
                    return;
                }

                if(!type){
                    $log.error("Could not find validation error type!");
                    return;
                }

                $scope.$watch(function () {
                    return !$scope[parentFormName][inputName].$error[type];
                },function(noErrors){
                    element.toggleClass('ng-hide',noErrors);
                });

            }
        };
    });

})();

The show-validation-errors can be added to a container of errors so that it will show/hide the container based upon a form fields validity.

and the show-validation-error shows or hides an element based upon that form fields validity on a given type.

An example of intended use:

        <form role="form" name="organizationForm" novalidate show-validation>
            <div class="form-group">
                <label for="organizationNumber">Organization number</label>
                <input type="text" class="form-control" id="organizationNumber" name="organizationNumber" required ng-pattern="/^[0-9]{3}[ ]?[0-9]{3}[ ]?[0-9]{3}$/" ng-model="organizationNumber">
                <div class="help-block with-errors" show-validation-errors="organizationNumber">
                    <div show-validation-error="required">
                        Organization number is required.
                    </div>
                    <div show-validation-error="pattern">
                        Organization number needs to have the following format "000 000 000" or "000000000".
                    </div>
                </div>
            </div>
       </form>
netbrain
  • 9,194
  • 6
  • 42
  • 68
2

I think it's too late to reply but hope you are going to love it:

CSS you can add other type of controls like select, date, password etc

input[type="text"].ng-invalid{
    border-left: 5px solid #ff0000;
    background-color: #FFEBD6;
}
input[type="text"].ng-valid{
    background-color: #FFFFFF;
    border-left: 5px solid #088b0b;
}
input[type="text"]:disabled.ng-valid{
    background-color: #efefef;
    border: 1px solid #bbb;
}

HTML: no need to add anything in controls except ng-required if it is

<input type="text"
       class="form-control"
       ng-model="customer.ZipCode"
       ng-required="true">

Just try it and type some text in your control, I find it really handy and awesome.

Ali Adravi
  • 21,707
  • 9
  • 87
  • 85
1

It's hard to tell for sure without a fiddle but looking at the angular.js code it does not replace classes - it just adds and removes its own. So any bootstrap classes (added dynamically by bootstrap UI scripts) should be untouched by angular.

That said, it does not make sense to use Bootstrap's JS functionality for validation at the same time as Angular - only use Angular. I would suggest you employ the bootstrap styles and the angular JS i.e. add the bootstrap css classes to your elements using a custom validation directive.

Marc
  • 13,011
  • 11
  • 78
  • 98
  • You are right, disabling native validation is the way to go. Yet I haven't been able to do that. I'll keep looking. Thanks! – Ivan P Jan 17 '13 at 20:27
1
<div class="form-group has-feedback" ng-class="{ 'has-error': form.uemail.$invalid && form.uemail.$dirty }">
  <label class="control-label col-sm-2" for="email">Email</label>
  <div class="col-sm-10">
    <input type="email" class="form-control" ng-model="user.email" name="uemail" placeholder="Enter email" required>
    <div ng-show="form.$submitted || form.uphone.$touched" ng-class="{ 'has-success': form.uemail.$valid && form.uemail.$dirty }">
    <span ng-show="form.uemail.$valid" class="glyphicon glyphicon-ok-sign form-control-feedback" aria-hidden="true"></span>
    <span ng-show="form.uemail.$invalid && form.uemail.$dirty" class="glyphicon glyphicon-remove-circle form-control-feedback" aria-hidden="true"></span>
    </div>
  </div>
</div>
Ajay Kumar
  • 4,864
  • 1
  • 41
  • 44
1

I know this is a very old question answer thread when I haven't heard the name of AngularJS itself :-)

But for others who land to this page looking for Angular + Bootstrap form validation in a clean and automated way, I've written a pretty small module for achieving the same without altering the HTML or Javascript in any form.

Checkout Bootstrap Angular Validation.

Following are the three simple steps:

  1. Install via Bower bower install bootstrap-angular-validation --save
  2. Add the script file <script src="bower_components/bootstrap-angular-validation/dist/bootstrap-angular-validation.min.js"></script>
  3. Add the dependency bootstrap.angular.validation to your application and that's it!!

This works with Bootstrap 3 and jQuery is not required.

This is based on the concept of jQuery validation. This module provides some additional validation and common generic messages for validation error.

Shashank Agrawal
  • 25,161
  • 11
  • 89
  • 121