Reconcile Angular.js and Bootstrap form validation styling

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.

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".

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...

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>

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);
                    });
                }
            }
        }
    };
}]);

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);
                        });
                    });
                }
            });
        }
    };
}]);

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.

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!

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>

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.

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.