Advanced form control with AngularJS and Bootstrap3
PDFForms in angular are pretty straightforward once you know how to read its state. So let’s look at a collection of tricks and put together and complete working Bootstrap 3 form with fabulous form validation.
In this form:
- No validation while initially filling the form
- On submit: validate form and keep updating fields as they change
- Red and green colors indicate the user’s progress
- Once submitted invalid form, the submit button becomes available only once the form is valid
- Bootstrap 3 form style, including help text and custom tailored alerts for validation messages
What AngularJS gives you
With AngularJS, <form>
is actually a built in directive that keeps track of the form’s validity status. It has features such as $pristine vs $dirty (did the user interact with the form or not), $valid vs $invalid and so on. Furthermore, it provides specific validations on input-basis. All of this information is available out of the box on the scope using expressions as you would normally. A form’s scope is available under the name of the form. So let’s take a look at a login form:
1 2 3 4 | <!-- disable button until form is valid --> <form name="loginForm" novalidate="novalidate"> <button type="submit" ng-disabled="loginForm.$invalid">log in!</button> </form> |
This form immediately displays validation errors even prior to submitting and the button is disabled while the form is in an invalid state.
Now this approach may satisfy your requirements, but this means you need to start showing form validation errors as soon as the form is visible, because you can’t submit the form until it is filled in correctly; not the best way to engage the user:
“Hey I need some information for you! Hey, I know you didn’t fill in anything, but everything is filled in wrong!”
Also notice we put in a novalidate to circumvent the browsers built in validation capabilities, since we want to let AngularJS and our own code to take care of that. (I wouldn’t assign a value to novalidate normally, but this code prettify plugin goes crazy otherwise).
On submit, validate form before sending request
One thing is missing is a flag that indicates if the form has been submitted. Often what you want to do is only show validation errors once the form has been submitted and then in runtime keep the user updated about his input fixes (still wrong or now it’s good). So there’s a small trick for that:
1 2 | <!-- inputs/labels will change style to error only when submitted also equals true --> <button type="submit">Submit</button> |
Now in your form you can use this scope variable to only show validation errors when submitted
is true
(for example ng-show="submitted && form.email.$error.required"
). A better example can be found on jsfiddle.
However, this does not help us much, because the form is still being submitted since we put in the novalidate attribute (the browser is not preventing a submit). So we need to prevent the form from submitting itself and we do that by introducing a directive that cooperates with the built in <form>
directive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // directive that prevents submit if there are still form errors app.directive('validSubmit', [ '$parse', function($parse) { return { // we need a form controller to be on the same element as this directive // in other words: this directive can only be used on a <form> require: 'form', // one time action per form link: function(scope, element, iAttrs, form) { form.$submitted = false; // get a hold of the function that handles submission when form is valid var fn = $parse(iAttrs.validSubmit); // register DOM event handler and wire into Angular's lifecycle with scope.$apply element.on('submit', function(event) { scope.$apply(function() { // on submit event, set submitted to true (like the previous trick) form.$submitted = true; // if form is valid, execute the submission handler function and reset form submission state if (form.$valid) { fn(scope, { $event : event }); form.$submitted = false; } }); }); } }; } ]); |
Now to use this!
1 2 3 4 5 6 7 8 9 | <div ng-app="app" ng-controller="MainCtrl"> <form name="form" valid-submit="sendFormToServer()" novalidate> <label for="name" ng-class="{'error': form.$submitted && form.name.$invalid}">name</label> <input id="name" name="name" type="text" required ng-model="name"></input> <button type="submit">log in!</button> <div class="help" ng-show="form.$submitted && form.name.$invalid">(name is required)</div> </form> </div> |
So this is basically all you need, but we can go further. We can make green what was filled in incorrectly and corrected by the user. Finally we can put some Bootstrap 3 sauce on top of this!
Let’s just cut to the chase:
Final solution in Bootstrap 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <div ng-app="app" ng-controller="MainCtrl"> <div class="panel-group"> <div class="panel panel-default"> <div class="panel-heading"> <h4 class="panel-title">Login form</h4> </div> <div class="panel-body"> <ng-include src="'form.html'"></ng-include> </div> </div> </div> <!-- kept seperate from the bootstrap markup to keep this example clean --> <script type="text/ng-template" id="form.html"> <form name="form" valid-submit="sendForm()" novalidate> <!-- call name--> <div class="form-group clearfix" ng-class="{ 'has-error': form.$submitted && form.callName.$invalid, 'has-success': form.$submitted && form.callName.$valid}"> <label class="col-sm-2 control-label" for="callName">Call Name</label> <div class="col-sm-5"> <input id="callName" name="callName" class="form-control" type="text" ng-model="person.callName" required autofocus></input> <div class="alert alert-danger" ng-show="form.$submitted && form.callName.$error.required">Call name is required</div> <p class="help-block">What do your parents and friends call you?</p> </div> </div> <!-- last name--> <div class="form-group clearfix" ng-class="{ 'has-error': form.$submitted && form.lastName.$invalid, 'has-success': form.$submitted && form.lastName.$valid}"> <label class="col-sm-2 control-label" for="lastName">Last Name</label> <div class="col-sm-5"> <input id="lastName" name="lastName" class="form-control" type="text" ng-model="person.lastName" required></input> <div class="alert alert-danger" ng-show="form.$submitted && form.lastName.$error.required">required</div> <p class="help-block">Last name as it is written on your passport</p> </div> </div> <!-- form controls--> <div class="form-group"> <button type="submit" class="btn btn-primary" ng-disabled="(form.$submitted && form.$invalid)">Submit!</button> </div> </form> </script> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | var app = angular.module('app', []); // directive that prevents submit if there are still form errors app.directive("validSubmit", [ "$parse", function($parse) { return { // we need a form controller to be on the same element as this directive // in other words: this directive can only be used on a <form> require: 'form', // one time action per form link: function(scope, element, iAttrs, form) { form.$submitted = false; // get a hold of the function that handles submission when form is valid var fn = $parse(iAttrs.validSubmit); // register DOM event handler and wire into Angular's lifecycle with scope.$apply element.on("submit", function(event) { scope.$apply(function() { // on submit event, set submitted to true (like the previous trick) form.$submitted = true; // if form is valid, execute the submission handler function and reset form submission state if (form.$valid) { fn(scope, { $event : event }); form.$submitted = false; } }); }); } }; } ]); // handle form submission when the form is completely valid app.controller("MainCtrl", function($scope) { $scope.sendForm = function() { alert('form valid, sending request...'); }; }); // focus on the first input when the page loads window.focus = function(selector) { // timeout is needed for Chrome (is a bug in Chrome) setTimeout(function(){ $(!!selector ? selector : "[autofocus]:not(:focus)").first().focus(); }, 1); }; |
1 2 3 4 | .form-group .alert { padding: 0px; margin-bottom: 0px; } |
Marcel
Nice! This saved me a lot of time…
robbie
gooood 🙂
Norman Klein
Shouldn’t the Submit button be disabled until the required fields are filled out? Also the required fields need to have a visual distinction (different border color or red field color or even a red asterisk as on this HTML page). Otherwise the user is compelled to press Submit to discover this info (resulting in an unnecessary click).
But this is still the best form validation I’ve yet seen for AngularJS + Bootstrap
Benny Bottema Post author
That’s a possible optimization, sure! It depends on preference I guess. Personally I like the form to be as passive and non-intrusive until absolutely necessary.
Jake
This was great and very helpful. I have a question about this line in the directive:
fn(scope, { $event : event });
I understand that it processes the submission handler function, however, I don’t know what the $event: event represents.
Benny Bottema Post author
It’s simply being passed on as the submission handler normally would have access to this event data. In this example however it is not used.
Jake
So I’m trying to do a watch in another directive on the form.$submitted property, but it doesn’t watch it properly:
angular.module('myApp')
.directive 'validateInput', [ () ->
require: '^form',
link: (scope, elem, attrs, ctrl) ->
parentDiv = $(elem).parent().closest('div')
scope.$watch 'ctrl.$submitted', () ->
console.log(ctrl)
if ctrl.$submitted && theInputIsValid (haven't added this in yet)
parentDiv.attr("class", 'has-success')
else
parentDiv.attr("class", 'has-error')
alertDiv = 'This field is required'
$(alertDiv).insertAfter(elem).parent()
, true
]
Sorry it’s in coffee script. This directive is placed on input fields within the form. I’m trying to automate adding the ng-class and alert divs that you have in this example, but have been banging my head on a wall for the past 3 days. Anyway, just thought I’d throw this out there for anyone else interested