Oleksiy's Blog

Extending services in AngularJS

04/06/2015

A good way to extend service for child module, is a good alternative for inheritance.

childModule.config(function($provide) {
    $provide. 
      decorator('parentFactory', function($delegate, childFactory) {
        return _.compose(childFactory, $delegate);
      });
  });

$delegate represents returned value of factory that being decorated. This decoration will be available within a paricular childModule. I declared my factories as function returned and used Underscore.js _.compose() method which returns a function which applies function passed through arguments continuously (leftwards) to returned value of previous one. If I had worked with object returned factories, I would probably apply _.extend($delegate, childFactory)

Inheritance vs Decoration

Decoration is not inheritance, since after inheritance you have two instances, and after decoration only one.

That does NOT work if you're going to have more than one extension within a module. That practice restricts you to extract it into separate modules and cause module hierarchy overhead, so consider that in your design.

As a positive thing in that is making you design your application better and disallows you to use super-class directly from child's module by braking the Law of Demeter.

Anyhow, that way you manipulate your dependencies in declarative way on level of configuration, not implementation, which is good!


Password Strength directive for AngularJS

10/28/2014

Have just written yet another tool password strength checker.

Module:

angular.module('myApp.strengthPassword', [])
    .directive('myPasswordStrength', ['passwordStrengthService',
        function (service) {

            function updateValues (password, scope) {
                password            = password || '';
                scope.lengthLevel   = service.getLengthLevel(password);
                scope.strengthLevel = service.getStrengthLevel(password);
                scope.strengthLabel = service.getLabel(scope.strengthLevel);
            }

            function link (scope) {
                scope.$watch('password', function(password) {
                    updateValues(password, scope);
                });
            }

            return {
                restrict: 'EA',
                replace: true,
                scope: { password: '=' },
                templateUrl: 'directives/strengthpassword/strengthpassword.tpl.html',
                link: link
            };
    }])

    .factory('passwordStrengthService', ['passwordStrengthConstants',
        function (constant) {
            var _matchPatterns  = constant.patterns;
            var _maxLengthLevel = constant.maxLengthLevel;

            function _getStrengthLevel (password) {
                for(var level in _matchPatterns) {
                    if (_matchPatterns[level].test(password)) { return level; }
                }
            }

            function _getLengthLevel (password) {
                var level = password.length / _maxLengthLevel * 100;
                return level < 100 ? level : 100;
            }

            function _getLabel (level) {
                return constant.labels[level];
            }

            return {
                getStrengthLevel    : _getStrengthLevel,
                getLengthLevel      : _getLengthLevel,
                getLabel            : _getLabel
            };
        }])

    .constant('passwordStrengthConstants', {
        patterns : {
            empty   : /^$/i,                                                   // not looping if empty
            strong  : /^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*\W).*$/, // 8+ sym, small+capital, digits, alpha
            medium  : /^.*(?=.{6,})(?=.*[a-z])(?=.*[\d\W]).*$/i,               // 6+ sym letters, digits
            weak    : /^.*(?=.{6,})(?=.*[a-z\d]).*$/i,                         // 6+ letters or digits
            useless : /^.*$/i                                                  // anything other
        },

        labels: {
            empty   : '',
            strong  : 'Good password!',
            medium  : 'Password is acceptable, but you could better',
            weak    : 'Your password is a piece of crap',
            useless : 'Password is too short'
        },

        maxLengthLevel : 20
    });

Template:

<div class="password-strength">
    <div class="password-strength-label">
        {{strengthLabel || 'js.passwordStrength'}}
    </div>
    <div class="bar-container">
        <div class="bar {{strengthLevel}}" style="width: {{lengthLevel}}%"></div>
    </div>
</div>

HTML:

<input type="password" name="password" id="password" ng-model="password" />
<div my-password-strength password="password"></div>

LESS/SASS:

.password-strength {
  margin-bottom: 10px;

  .bar-container {
    background: #eee;
    margin-bottom: 10px;

    .password-strength-label {
      margin-bottom: 5px;
    }

    .bar {
      height: 5px;
      -ms-transition:     width .5s ease;
      -webkit-transition: width .5s ease;
      transition:         width .5s ease;

      &.strong  { background: #008641; }
      &.medium  { background: #2573d9; }
      &.weak    { background: #f60; }
      &.useless { background: #e51400; }
    }
  }
}