AngularJs Password Strength Indicator with Bootstrap
Building a password strength indicator using AngularJS and Bootstrap
TL ;DR: Code
I kept thinking there’s something wrong with ng-password-strength so in the end, I decided to build my own indicator from scratch by being inspired by stackoverflow answer http://stackoverflow.com/a/21833310/971085. Shouldn’t be too hard for someone that had little experience with Angular right? Hmm, wrong. Or maybe I’m just a very very slow learner.
So here’s my guide on how I built an indicator that works with pattern validator also.
First, the html:
<div ng-app="passwordModule" ng-controller="credentialsController" class="container">
<form name="form">
<div class="form-group">
<label for="password">Password</label>
<input
type="text"
name="password"
id="password"
ng-model="credentials.password"
ng-model-options="{allowInvalid: true}"
pattern-validator="((?=.*\d)(?=.*[A-Z])(?=.*\W).{8,8})"
class="form-control"
/>
</div>
<div class="form-group">
<label>Password Strength</label>
<password-strength ng-model="credentials.password"></password-strength>
</div>
<div class="alert alert-error" ng-show="form.password.$error.passwordPattern">
The password does not meet requirements!
</div>
</form>
</div>
Very simple bootstrap form with a normal text input. The key to the html, is that we need the ability to move the directive anywhere in our html without embedding other validation inside it. Unlike the stackoverflow post, which ends up having regex validation and strength validation in one.
On to Javascript:
angular
.module('passwordModule', [])
.controller('credentialsController', [
'$scope',
function ($scope) {
// Initialise the password as hello
$scope.credentials = {
password: 'hello'
};
}
])
.directive('passwordStrength', [
function () {
return {
require: 'ngModel',
restrict: 'E',
scope: {
password: '=ngModel'
},
link: function (scope, elem, attrs, ctrl) {
scope.$watch(
'password',
function (newVal) {
scope.strength =
isSatisfied(newVal && newVal.length >= 8) +
isSatisfied(newVal && /[A-z]/.test(newVal)) +
isSatisfied(newVal && /(?=.*\W)/.test(newVal)) +
isSatisfied(newVal && /\d/.test(newVal));
function isSatisfied(criteria) {
return criteria ? 1 : 0;
}
},
true
);
},
template:
'<div class="progress">' +
'<div class="progress-bar progress-bar-danger" style="width: {{strength >= 1 ? 25 : 0}}%"></div>' +
'<div class="progress-bar progress-bar-warning" style="width: {{strength >= 2 ? 25 : 0}}%"></div>' +
'<div class="progress-bar progress-bar-warning" style="width: {{strength >= 3 ? 25 : 0}}%"></div>' +
'<div class="progress-bar progress-bar-success" style="width: {{strength >= 4 ? 25 : 0}}%"></div>' +
'</div>'
};
}
])
.directive('patternValidator', [
function () {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, elem, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var patt = new RegExp(attrs.patternValidator);
var isValid = patt.test(viewValue);
ctrl.$setValidity('passwordPattern', isValid);
// angular does this with all validators -> return isValid ? viewValue : undefined;
// But it means that the ng-model will have a value of undefined
// So just return viewValue!
return viewValue;
});
}
};
}
]);
The passwordStrength directive is very simple. It requires the ngModel directive to specify which model (scope property) we need to watch when it changes.
Make note of [scope : {password : “=ngModel”}]
. It took a very long time to figure out how to get the ngModel value
in to our scope. Somehow, I dug through google to eventually find this and it made me pull hairs out because it felt
like another hidden feature! This is the point when my hatred toward AngularJS grows a little more …
The scope.$watch
method does all the work, and this is very simple. It gets the new password value and determines the strength
and stores it on the scope. The directive template has the bootstrap progress bars and displays each based on the strength
value 1/2/3 or 4.
template: '
<div class="progress">
' + '
<div class="progress-bar progress-bar-danger" style="width: {{strength >= 1 ? 25 : 0}}%"></div>
' + '
<div class="progress-bar progress-bar-warning" style="width: {{strength >= 2 ? 25 : 0}}%"></div>
' + '
<div class="progress-bar progress-bar-warning" style="width: {{strength >= 3 ? 25 : 0}}%"></div>
' + '
<div class="progress-bar progress-bar-success" style="width: {{strength >= 4 ? 25 : 0}}%"></div>
' + '
</div>
'
So, at this point we have pretty much replicated the ng-password-strength except with a different template and logic but same idea. At this point I decided to include ng-pattern validation directive from Angular itself and I placed it inside the password field and that way we have the form validation working.
However, when you start typing in the password box the password strength indicator fails to display anything. The
reason for this is (in my opinion), very silly and un-obvious. The ng-pattern directive marks the element as $dirty
and $invalid
which prints the validation message - nice. But, the password value on the scope is now undefined which
makes our password indicator useless! This also took a very long time to figure out.
The fix in the end was to have my own patterValidator. This also, requires the ngModel and this time a 4th parameter in the link function is required which is the ngModel Controller!
To use this controller I found once again, very confusing and complex!
The key line is:
ctrl.$parsers.unshift(function (viewValue) {});
From what I can tell, there is a $parsers array property on the controller. We need to add our function at the beginning of that array by doing the unshift. So our function takes the newValue and apparently this will get hit every time the password changes. Once again, I found a key syntax in Angular that has very little documentation. Another sign of a ‘bad’ framework. Things like this should very simple to do and obvious even to beginner developers.
Finally, the key fix…
return isValid ? viewValue : undefined
Why not just return viewValue?! It should be up to the developer to determine whether the form submit should go ahead even with invalid values. If it goes to the server, there should be server validation anyway to protect the data. Oh, what a day…
Thanks and happy coding!