Joel's Thoughts

Building A Password Strength Directive In AngularJs

July 10, 2016

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.

  1. status - defaults to “Invalid”.
  2. value - defaults to an empty string. Refers to the value on the input. This updates as soon as the user starts typing.
  3. meterWidth - binded to the view as the css width on a meter container. Responsible for the effect of filling a div with red background as the password comes near to a “Valid” state.
  4. type - defaults to string “password” and is binded to the view as the input’s type property. This changes to “text” once the user click the checkbox.
  5. 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.









  • About
  • Search
  • Resume
  • Powered by Jekyll using the Trio theme