Recently, I am working on a huge front end project and was tasked to rewrite the existing password strength widget for bug fixes and maintainability. I ended up rewriting the widget in jQuery due to the client’s requirement. However, I’m thinking that an AngularJs version of the widget could be useful on my other projects. For re-usability purposes, building the widget as an AngularJs directive is the best approach.
Here’s the demo using AngularJs. View source code on Github.
I start by creating the skeleton of my directive code.
'use strict';
angular.module('app.widget', [])
.directive('passwordStrength', function() {
return {
templateUrl: 'password-strength-widget.html',
restrict: 'E',
scope: {},
controller: function ($scope) {
},
link: function ($scope, $element, $attrs, $ctrl) {
}
};
});
The directive’s template or view is pointing to an existing file named “password-strength-widget.html”. This will hold the widget’s html code. Scope property is an empty object. This ensures that we isolate the directive’s scope and not inherit the parent.
I set the widget’s name as passwordStrength and the directive’s restrict property to E. Consequently, I can add the widget on my AngularJs project like this.
<password-strength></password-strength>
Building the main properties for the password.
'use strict';
angular.module('app.widget', [])
.directive('passwordStrength', function() {
return {
templateUrl: 'password-strength-widget.html',
restrict: 'E',
scope: {},
controller: function ($scope) {
//declare password scope property
$scope.password = {
status: 'Invalid',
value: '',
meterWidth: 0,
type: 'password',
rules:{
isValidLength: false,
hasNumber: false,
hasLetter: false,
noSpecialChar: false
}
};
},
link: function ($scope, $element, $attrs, $ctrl) {
}
};
});
I added a new property on the directive’s scope to be binded on the template/view.
The password object has the following properties.
rules - an object that holds the rules set on our password widget. Currently contains 4 rules that determine if a password is valid.
a. isValidLength - true if password's length is greater than or equal to 6 otherwise false.
b. hasNumber - true if password contains a number otherwise false.
c. hasLetter - true if password contains a letter otherwise false.
d. noSpecialChar - true if password has no space or forward slash.
Creating the function that sets the default properties on initialization.
'use strict';
angular.module('app.widget', [])
.directive('passwordStrength', function() {
return {
templateUrl: 'password-strength-widget.html',
restrict: 'E',
scope: {},
controller: function ($scope) {
//declare password scope property
$scope.password = {
status: 'Invalid',
value: '',
meterWidth: 0,
type: 'password',
rules:{
isValidLength: false,
hasNumber: false,
hasLetter: false,
noSpecialChar: false
}
};
//set the default values
this.setDefault = function(password){
password.rules.hasNumber = false;
password.rules.hasLetter = false;
password.rules.isValidLength = false;
password.rules.noSpecialChar = true;
$scope.password.meterWidth = 25;
};
},
link: function ($scope, $element, $attrs, $ctrl) {
}
};
});
Take note that noSpecialChar
is set to true. This is because the input value is an empty string at first.
Also, I set meterWidth
to 25 at first. 1 out of 4 rules is true so this results to 25%.
Obviously, I need a function that will recalculate the meterWidth
as soon as the user updates the password.
.... more code
controller: function ($scope) {
//declare password scope property
$scope.password = {
status: 'Invalid',
value: '',
meterWidth: 0,
type: 'password',
rules:{
isValidLength: false,
hasNumber: false,
hasLetter: false,
noSpecialChar: false
}
};
//set the default values
this.setDefault = function(password){
password.rules.hasNumber = false;
password.rules.hasLetter = false;
password.rules.isValidLength = false;
password.rules.noSpecialChar = true;
$scope.password.meterWidth = 25;
};
//recalculate the meterWidth
this.getMeterWidth = function(password){
var property_count = 0, valid_property_count = 0, property;
for (property in password.rules) {
if (password.rules.hasOwnProperty(property)) {
property_count = property_count + 1;
if(password.rules[property]){
valid_property_count = valid_property_count + 1;
}
}
}
return (valid_property_count/property_count)*100;
};
},
.... more code
Function getMeterWidth
main task is to loop through each password rules property. If the property’s value is true, increment the valid_property_counter
variable.
After looping, calculate and return the percentage value of the valid_property_counter
against the total rule count.
Next create a function to determine the password status. I call it getStatus
.
.... more code
//recalculate the meterWidth
this.getMeterWidth = function(password){
var property_count = 0, valid_property_count = 0, property;
for (property in password.rules) {
if (password.rules.hasOwnProperty(property)) {
property_count = property_count + 1;
if(password.rules[property]){
valid_property_count = valid_property_count + 1;
}
}
}
return (valid_property_count/property_count)*100;
};
//get the password status
this.getStatus = function(password){
if(100 === password.meterWidth){
return 'Valid';
}
return 'Invalid';
};
},
.... more code
Function getStatus
main job is to check the password meterWidth
value. If value is not equal to 100, it will return “Invalid” else “Valid”.
Inside directives’ link function, I began to start using all the controller’s functions I previously wrote.
.... more code
//get the password status
this.getStatus = function(password){
if(100 === password.meterWidth){
return 'Valid';
}
return 'Invalid';
};
},
link: function ($scope, $element, $attrs, $ctrl) {
//call the default function
$ctrl.setDefault($scope.password);
//listen to password value change
$scope.onPasswordChange = function(password){
$ctrl.setDefault(password);
//set password rules if valid or invalid using regex
if(password.value){
password.rules.hasNumber =
password.value.match(/\d/) ? true:false;
password.rules.hasLetter =
password.value.match(/[A-z]/) ? true:false;
password.rules.isValidLength =
password.value.match(/^.{6,}$/) ? true:false;
password.rules.noSpecialChar =
!password.value.match(/[ /"]/) ? true:false;
}
//set the meter width
password.meterWidth = $ctrl.getMeterWidth(password);
//set the status
password.status = $ctrl.getStatus(password);
};
}
... more code
It’s important to take note of the new scope function onPasswordChange
. This is binded on the view using the ng-change
directive for the input.
I could use $watch
however ng-change
is optimal in performance for this situation.
Next is to create the directive’s template/view. This is how the html code looks inside the password-strength-widget.html.
<div class="password-strength-widget">
<label for="password">
Create Password
</label>
<br>
<input id="password" name="password" type="{{password.type}}"
ng-trim="false" ng-model="password.value"
ng-change="onPasswordChange(password)">
<label for="show-password">
<input type="checkbox" name="show-password"
id="show-password" ng-model="password.type"
ng-true-value="text" ng-false-value="password">
Show Password
</label>
<br>
<br>
<span>{{password.status}}</span>
<div class="meter-wrapper">
<div ng-class="{ 'danger' : password.meterWidth < 100 }"
style="width:{{password.meterWidth}}%;">
</div>
</div>
<br>
<ul>
<li ng-class="{ 'invalid' : !password.rules.hasNumber }">
At least one number (0-9)
</li>
<li ng-class="{ 'invalid' : !password.rules.hasLetter }">
At least one letter (a-z)
</li>
<li ng-class="{ 'invalid' : !password.rules.isValidLength }">
At least 6 characters
</li>
<li ng-class="{ 'invalid' : !password.rules.noSpecialChar }">
No spaces, forward slashes (/) or double quote marks (")
</li>
</ul>
</div>
If you’re familiar with AngularJs, the html code is pretty straightforward. One important thing here is the ng-trim
directive of the input being set to false.
By default, this directive is set to true. This means that if a user press an empty space or a tab, the password’s value will always be trimmed ( without spaces ), consequently making
the rule for no spaces (password.rules.noSpecialChar
) unreliable.
The template’s style,
.password-strength-widget ul{
padding-left: 0;
}
.password-strength-widget ul li {
background: none;
padding: 0 0 5px 18px;
line-height: 15px;
margin: 0;
list-style-type: none;
position: relative;
}
.password-strength-widget ul li:before {
position: absolute;
top: 0;
left: 1px;
font-size: 22px;
}
.password-strength-widget li.invalid {
color: #d40000;
}
.password-strength-widget li.invalid:before{
content: "\d7";
color: #d40000;
}
.password-strength-widget li:before{
content: "\2713";
font-size: 16px;
}
.password-strength-widget .meter-wrapper {
width: 140px;
height: 22px;
border: 1px solid #ccc;
margin-bottom: 12px;
}
.password-strength-widget .meter-wrapper div{
height:20px;
background-color: #6cb33e;
}
.password-strength-widget .meter-wrapper div.danger {
background-color: #d40000;
}
Here’s the whole directive code.
'use strict';
angular.module('app.widget', [])
.directive('passwordStrength', function() {
return {
templateUrl: 'password-strength-widget.html',
restrict: 'E',
scope: {},
controller: function ($scope) {
$scope.password = {
status: 'Invalid',
value: '',
meterWidth: 0,
type: 'password',
rules:{
isValidLength: false,
hasNumber: false,
hasLetter: false,
noSpecialChar: false
}
};
//set default values
this.setDefault = function(password){
password.rules.hasNumber = false;
password.rules.hasLetter = false;
password.rules.isValidLength = false;
password.rules.noSpecialChar = true;
$scope.password.meterWidth = 25;
};
this.getMeterWidth = function(password){
var property_count = 0,
valid_property_count = 0,
property;
for (property in password.rules) {
if (password.rules.hasOwnProperty(property)) {
property_count = property_count + 1;
if(password.rules[property]){
valid_property_count =
valid_property_count + 1;
}
}
}
return (valid_property_count/property_count)*100;
};
this.getStatus = function(password){
if(100 === password.meterWidth){
return 'Valid';
}
return 'Invalid';
};
},
link: function ($scope, $element, $attrs, $ctrl) {
$ctrl.setDefault($scope.password);
//listen to password change
$scope.onPasswordChange = function(password){
$ctrl.setDefault(password);
//set password rules if valid or invalid using regex
if(password.value){
password.rules.hasNumber =
password.value.match(/\d/) ? true:false;
password.rules.hasLetter =
password.value.match(/[A-z]/) ? true:false;
password.rules.isValidLength =
password.value.match(/^.{6,}$/) ? true:false;
password.rules.noSpecialChar =
!password.value.match(/[ /"]/) ? true:false;
}
password.meterWidth = $ctrl.getMeterWidth(password);
password.status = $ctrl.getStatus(password);
};
}
};
});
To use the directive in my other AngularJs application, I would do something like this.
angular.module('app', ['app.widget'])
.controller('MainController', ['$scope', function($scope) {
//your controller's code
}]);
And on my view.
<div ng-app="app">
<div ng-controller="MainController">
<password-strength>
</password-strength>
</div>
</div>
View the demo or get source code on Github.