Reconcile Angular.js and Bootstrap form validation styling

Twitter BootstrapAngularjs

Twitter Bootstrap Problem Overview


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.

Twitter Bootstrap Solutions


Solution 1 - Twitter Bootstrap

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

Solution 2 - Twitter Bootstrap

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

Solution 3 - Twitter Bootstrap

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>

Solution 4 - Twitter Bootstrap

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

Solution 5 - Twitter Bootstrap

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

Solution 6 - Twitter Bootstrap

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.

Solution 7 - Twitter Bootstrap

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!

Solution 8 - Twitter Bootstrap

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>

Solution 9 - Twitter Bootstrap

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.

Solution 10 - Twitter Bootstrap

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.

Solution 11 - Twitter Bootstrap

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

Solution 12 - Twitter Bootstrap

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionIvan PView Question on Stackoverflow
Solution 1 - Twitter BootstrapwhuhackerView Answer on Stackoverflow
Solution 2 - Twitter BootstrapmalixView Answer on Stackoverflow
Solution 3 - Twitter BootstrapfarinczView Answer on Stackoverflow
Solution 4 - Twitter BootstrapemertechieView Answer on Stackoverflow
Solution 5 - Twitter BootstrapJason ImView Answer on Stackoverflow
Solution 6 - Twitter BootstrapTom SpencerView Answer on Stackoverflow
Solution 7 - Twitter BootstrapBen LeshView Answer on Stackoverflow
Solution 8 - Twitter BootstrapnetbrainView Answer on Stackoverflow
Solution 9 - Twitter BootstrapAli AdraviView Answer on Stackoverflow
Solution 10 - Twitter BootstrapMarcView Answer on Stackoverflow
Solution 11 - Twitter BootstrapAjay KumarView Answer on Stackoverflow
Solution 12 - Twitter BootstrapShashank AgrawalView Answer on Stackoverflow