Thursday, October 20, 2016

TemplateUrl Function for Dynamic Templating

I've come across multiple situations while using Angular JS where I'd like to dynamically load a template into a directive.  It used to be pretty difficult, but in more recent versions (I'm not sure when it was introduced) you can pass a function to templateUrl to somewhat dynamically determine which template you want to use.  Most recently this came in handy when I was nesting a bunch of tables within each other (yes, it was a valid use case), but I also wanted those tables to be available alone.

I made a template for each table - 5 total - and then passed the relative url of the template I wanted as an attribute to the directive.  So it worked out like this:

<ea-children details="childrenFirst" template-url="/templates/first.html"></ea-children>
<ea-children details="childrenSecond" template-url="/templates/second.html"></ea-children>
<ea-children details="childrenThird" template-url="/templates/third.html"></ea-children>

As you can see I was able to reuse the same directive three times with potentially drastically different markup.  The templateUrl function looks like this:

templateUrl: function (element, attributes) {
    return attributes.templateUrl;
}

One major item to note is that you don't have access to scope variables because the template is requested before scope is initialized.

Hopefully next time I need to do this (assuming I'm still using Angular 1.x) I'll remember to check here first.  Happy coding!

Tuesday, October 11, 2016

Angular Tree View

For the second (or maybe third) time I faced a situation that would drastically benefit from a Tree View control.  Since I love Angular and was using it anyway, I finally set out to create a Tree View control I could reuse and be proud of.  It's pretty basic, but that's kinda the point.  There's actually a little bit of styling on this one, too.  Once again I'm going to try to get it up on Github at some point, but for now you can check it out on plnkr.

UPDATE: I've changed the directive significantly, checked it into Github and also created an npm package out of it called angular-tree-view.  I'm still updating a lot of it, but it's alive and in the wild now!

Check it out on Github
Check it out on npm

An important piece to note is that this feature actually uses two different directives nested within each other.  There's the parent item, which is a tree-view and then there's the child item, which is a tree-view-item.  Each tree-view-item can have another tree-view in it, which can then have another tree-view-item, which can then have another tree-view, etc.

Directive Requirements:
  • Each item can have an unlimited number of sub-items
  • Each sub-item can have an unlimited number of sub-items (ad infinitum)
  • The user can specify a callback to be invoked when a sub-item with no sub-items of its own is clicked
  • The selected item should be indicated by the application of an "active-branch" class
Directive Module Code:
angular.module('ea.treeview', []).directive('eaTreeView', function() {
  return {
        restrict: 'E',
        scope: {
            items: '=',
            isRoot: '@',
            callback: '&'
        },
        link: function(scope) {
            scope.callback = scope.callback();
        },
        controller: function ($scope) {
            this.isRoot = $scope.isRoot;
        },
        template: '<ea-tree-view-item item="item" callback="callback" data-ng-repeat="item in items"></ea-tree-view-item>'
    }
}).directive('eaTreeViewItem', function() {
  return {
        restrict: 'E',
        scope: {
            item: '=',
            callback: '&'
        },
        require: '^eaTreeView',
        link: function (scope, element, attributes, controller) {
          scope.callback = scope.callback();
            scope.activate = function () {
              var activeElements = document.getElementsByClassName('active-branch');
              angular.forEach(activeElements, function(activeElement){
                angular.element(activeElement).removeClass('active-branch');
              });
              element.children('div').addClass('active-branch');
              scope.callback(scope.item);
            };

            scope.hasChildren = function () {
                return !!scope.item.items && !!scope.item.items.length;
            };

            scope.hasParent = function() {
                return !controller.isRoot;
            };

            scope.toggleExpanded = function() {
                scope.item.expanded = !scope.item.expanded;
            };
        },
        templateUrl: 'treeViewItem.html'
    }
});

The treeViewItem Template:
<div>
    <div data-ng-if="hasChildren()" class="tree-parent" data-ng-class="{'tree-child': hasParent()}">
        <div data-ng-click="toggleExpanded()" class="clickable">
            <i class="fa" data-ng-class="{'fa-chevron-right': !item.expanded, 'fa-chevron-down': item.expanded}"></i>
            <span>{{item.display}}</span>
        </div>
        <ea-tree-view data-ng-if="item.expanded" callback="callback" items="item.items"></ea-tree-view>
    </div>
    <div data-ng-if="!hasChildren()" data-ng-click="activate()" class="tree-child clickable">{{item.display}}</div>
</div>

A dash of styling:
.clickable {
  cursor: pointer;
}

.active-branch {
    background-color: lightgrey;
}

.tree-parent {
  padding-left: 0;
}

.tree-child {
  padding-left: 1em;
}

.tree-final-child {
  padding-left: 2em;
}

And finally some usage:
<ea-tree-view items="model.items" is-root="true" callback="go"></ea-tree-view>

angular.module('demo', ['ea.treeview']).controller('mainController', function($scope) {
  $scope.model = {
    items: [
      {
        display: 'Abe',
        items: [
          {display: 'Homer', items: [{display: 'Bart'}, {display: 'Lisa'}, {display: 'Maggie'}]},
          {display: 'Herb'},
          {display: 'Abbie'}
        ]
      },
      {
        display: 'Jacqueline',
        items: [
          {display: 'Patty'},
          {display: 'Selma'},
          {display: 'Marge', items: [{display: 'Bart'}, {display: 'Lisa'}, {display: 'Maggie'}]}
        ]
      }
    ]
  };
  
  $scope.go = function (item) {
      console.log("You could have navigated!", item);
  };
});

Angular Tri-State Loading Directive

I had a situation where I wanted to load some data asynchronously and display a visual cue to the user that the action had been completed.  However, there was a possibility that more than one result could be returned and the user would have to take some action to confirm which item they want to use.  I also wanted to show the user a "loading" message so they'd have the visual cue that something was happening.  Since I was already using Angular and Font Awesome I decided the best way to go about it would be to use a directive.  I prefer my directives to be configurable as much as possible so I can reuse them in the future with relative ease (that is the point of directives after all).  This is what I came up with.  I will at some point get it uploaded to Github, but for now this is the code.  (You can check it out on plnkr, too).

First I had to create the directive.  I've refactored it a little bit so that it's usable as a separate module by just including 'ea.loading' in your module import list when you declare your module.

Directive Requirements:

  • User can configure which icons to use, but defaults will be used if nothing is specified
  • User can configure which color(s) to use on which states and no defaults will be used
  • User can specify a callback function and parameters for each of the three states separately (for click events only)
  • User can specify an initial state, but a default of 0 (do not show) will be used if nothing is specified

Directive Module Code:
angular.module('ea.loading', []).directive('eaTriStateLoading', function() {
  return {
    restrict: 'E',
    scope: {
      animate: '@',
      initialValue: '@',
      loadingCallback: '&',
      loadingCallbackParameters: '=',
      loadingClass: '@',
      loadingColor: '@',
      notOkCallback: '&',
      notOkClass: '@',
      notOkColor: '@',
      okCallback: '&',
      okClass: '@',
      okColor: '@',
      state: '='
    },
    link: {
      pre: function(scope, element, attributes) {
        scope.animate = scope.animate || true;
        scope.state = parseInt(scope.initialValue) || 0;
        scope.loadingClass = scope.loadingClass || 'fa-spinner';
        scope.okClass = scope.OkClass || 'fa-check';
        scope.notOkClass = scope.notOkClass || 'fa-exclamation-triangle';
      },
      post: function(scope, element, attributes) {
        function buildParameterObject(parameters) {
          var result = {};
          
          for (var i = parameters.length; --i >= 0;) {
            var parameter = parameters[i];
            for (var j = Object.keys(parameter).length; --j >= 0;) {
              // key is the name of the parameter
              // parameter[key] is the name of the property on scope that will contain the value to be passed to the parameter
              var key = Object.keys(parameter)[j];
              var value = scope.$parent[parameter[key]] || parameter[key].substring(1, parameter[key].length - 1);
              result[key] = value;
            }
          }
          
          return result;
        }
        
        scope.click = function($event) {
          var parameter = {};
          if (scope.state === 1 && !!scope.loadingCallback && angular.isDefined(scope.loadingCallback) && angular.isFunction(scope.loadingCallback)) {
            if (!!scope.loadingCallbackParameters) {
              parameter = buildParameterObject(scope.loadingCallbackParameters);
            }
            parameter.$event = $event;
            scope.loadingCallback(parameter);
          } else if (scope.state === 2 && angular.isDefined(scope.okCallback) && angular.isFunction(scope.okCallback)) {
            if (!!scope.okCallbackParameters) {
              parameter = buildParameterObject(scope.okCallbackParameters);
            }
            parameter.$event = $event;
          } else if (scope.state === 3 && angular.isDefined(scope.noOkCallback) && angular.isFunction(scope.noOkCallback)) {
            if (!!scope.notOkCallbackParameters) {
              parameter = buildParameterObject(scope.notOkCallbackParameters);
            }
            parameter.$event = $event;
            scope.notOkCallback(parameter);
          }
        };
      },
    },
    template: '<i class="fa" ng-class="{\'{{loadingClass}}\': state === 1, \'fa-pulse\': state === 1 && animate === true, \'{{okClass}}\': state === 2, \'{{notOkClass}}\': state === 3}" ng-click="click($event)" ng-style="{color: (state === 1 ? loadingColor : state === 2 ? okColor : notOkColor)}"></i>'
  }
});

And some use cases:
<ea-tri-state-loading loading-callback-parameters="model.firstDemoLoadingCallbackParameters" loading-callback="clickCallback(property, model.age, $event)" loading-color="blue" not-ok-color="red" ok-callback="decrement()" ok-color="green" state="model.firstDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading loading-class="fa-circle-o-notch" loading-callback-parameters="[{'property': '\'secondDemoValue\''}]" loading-callback="clickCallback(property, $event)" loading-color="purple" not-ok-color="yellow" ok-color="orange" initial-value="2" state="model.secondDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading animate="false" ok-class="fa-rocket" state="model.thirdDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading not-ok-class="fa-remove" state="model.fourthDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading state="model.fifthDemoValue"></ea-tri-state-loading>
And finally the controller that would back those use cases:
angular.module('demo', ['ea.loading']).controller('mainController', function($scope) {
  $scope.states = {
    first: 1,
    second: 2,
    third: 3
  };
  $scope.model = {
    firstDemoValue: 1,
    secondDemoValue: 1,
    thirdDemoValue: 1,
    fourthDemoValue: 2,
    fifthDemoValue: 3,
    age: 12,
    firstDemoLoadingCallbackParameters: [
      {'property': '"firstDemoValue"'},
      {'value': 'age'}
    ]
  };
  
  $scope.clickCallback = function(property, value, $event) {
    console.log('property', property);
    console.log('value', value);
    console.log('$event', $event);
  };
  
  $scope.simulateLoading = function(property) {
    $scope.model[property] = 1;
  }
  
  $scope.simulateOk = function(property) {
    $scope.model[property] = 2;
  };
  
  $scope.simulateNotOk = function(property) {
    $scope.model[property] = 3;
  };
});

Wednesday, September 14, 2016

Order of Import

Hi! It's been a while and I know you missed me.  Awwww, you're sweet.  I missed you, too.

Anyway, I had an issue this morning that I wanted to document in case it happens again. I'm setting up unit testing on a new project with Karma and Jasmine and I was getting a weird error for a while.  Specifically, I was getting "TypeError: Attempted to assign to readonly property" and it was driving me crazy (my wife would tell you that's quite a short drive).  Eventually I figured out that my scripts were being referenced in the wrong order in my Karma config file.  Because of alphabetization I was importing angular-mocks.js before angular.min.js (apparently hyphens come before periods).  I switched them around and voila! Now I'm all set again.

Bonus: I was also running into several weird errors with angular-mocks until I went back and grabbed an older version. Since this project is using AngularJS 1.3.5 I went back to angular-mocks 1.3.19 and now everything works as expected.

Tuesday, August 2, 2016

Free eBooks!!!!

It seems like about once per year Microsoft decides to give away a ton of eBooks just because they can.  Well, it's that time of year.  If you go here you can view all of the books that are available and download them one at a time.  Alternately, you can paste the following into a word document, save it as "index.html" without the quotes, then open that file in your web browser to download all of them at once.  That's what I did and it took about 30 minutes over WiFi at home (I have high speed Internet access so if you happen to be using dial-up, you'll want to keep that in mind).  I just noticed that the last few downloads failed because I lost my connection due to a storm while I was downloading.  I have no way of knowing which files were downloaded now.  Ugh.  But whatever, I can't do everything for you.

I don't need all of these books, but I'm a big fan of having resources and not needing them instead of needing them and not having them.  And since Eric says right there on his blog that he wants us to download all of these, I felt obliged to do so.  Happy reading!

<html>
 <head>
  <title>Download eBooks</title>
 </head>
 <body>
  <script>
   (function() {
    var endpoints = [
     "http://ligman.me/29ngkYn",
     "http://ligman.me/29jL5wW",
     "http://ligman.me/29afIRV",
     "http://ligman.me/29pyHgR",
     "http://ligman.me/29dmbfC",
     "http://ligman.me/29ollRF",
     "http://ligman.me/29gvv67",
     "http://ligman.me/29pzkHg",
     "http://ligman.me/29CWQ20",
     "http://ligman.me/1G0Cm7T",
     "http://ligman.me/29vDVrw",
     "http://ligman.me/29CW4SV",
     "http://ligman.me/29G1z1N",
     "http://ligman.me/29yWAAO",
     "http://ligman.me/29DHWHq",
     "http://ligman.me/1HGwMgm",
     "http://ligman.me/29yUVeH",
     "http://ligman.me/29ErxWs",
     "http://ligman.me/29EsHkr",
     "http://ligman.me/29v5zCF",
     "http://ligman.me/1H32nUT",
     "http://ligman.me/29yixk9",
     "http://ligman.me/29vq6d2",
     "http://ligman.me/29cvrjG",
     "http://ligman.me/29pzjTR",
     "http://ligman.me/29rPOf6",
     "http://ligman.me/29ctW9z",
     "http://ligman.me/29gvoHt",
     "http://ligman.me/29gvfEd",
     "http://ligman.me/29LvcBg",
     "http://ligman.me/29DS3gJ",
     "http://ligman.me/1q9L65I",
     "http://ligman.me/29pufLO",
     "http://ligman.me/29wODdA",
     "http://ligman.me/29vch9q",
     "http://ligman.me/29EmfZc",
     "http://ligman.me/29LXmMt",
     "http://ligman.me/29qwKfQ",
     "http://ligman.me/29sp3nZ",
     "http://ligman.me/29yWFFf",
     "http://ligman.me/29ZNaMN",
     "http://ligman.me/29gYy5h",
     "http://ligman.me/29NhudA",
     "http://ligman.me/29jDRJf",
     "http://ligman.me/29conIf",
     "http://ligman.me/29cnLlL",
     "http://ligman.me/29fNYRE",
     "http://ligman.me/29cq9cw",
     "http://ligman.me/29acqhj",
     "http://ligman.me/29fJJCS",
     "http://ligman.me/29nbxWK",
     "http://ligman.me/29fO9MV",
     "http://ligman.me/29pr2x1",
     "http://ligman.me/29oa0kH",
     "http://ligman.me/29i6ntm",
     "http://ligman.me/29n7JVF",
     "http://ligman.me/29fQsQ4",
     "http://ligman.me/29fQo2J",
     "http://ligman.me/29idz8T",
     "http://ligman.me/29dfwlu",
     "http://ligman.me/29fLliP",
     "http://ligman.me/29i83TI",
     "http://ligman.me/29diypX",
     "http://ligman.me/29fNpHO",
     "http://ligman.me/29oefNf",
     "http://ligman.me/29jGTgC",
     "http://ligman.me/29crPhL",
     "http://ligman.me/29FUJIk",
     "http://ligman.me/1G2oDNG",
     "http://ligman.me/29u5uMT",
     "http://ligman.me/29rlRt3",
     "http://ligman.me/29Yd9V0",
     "http://ligman.me/29s4EQ1",
     "http://ligman.me/29vkv6E",
     "http://ligman.me/29A3q8Z",
     "http://ligman.me/29H7ovO",
     "http://ligman.me/1dHMOui",
     "http://ligman.me/1G2pw8X",
     "http://ligman.me/29gnaPK",
     "http://ligman.me/29d7GZH",
     "http://ligman.me/29i2Mvf",
     "http://ligman.me/29rbpkY",
     "http://ligman.me/29e6cTO",
     "http://ligman.me/29hJU1c",
     "http://ligman.me/29bcJtd",
     "http://ligman.me/29bcRZO",
     "http://ligman.me/29d5mS2",
     "http://ligman.me/29cz6OD",
     "http://ligman.me/29fDult",
     "http://ligman.me/29pEdjN",
     "http://ligman.me/29rwoY0",
     "http://ligman.me/29fTQrx",
     "http://ligman.me/29gfrkN",
     "http://ligman.me/29rwoHw",
     "http://ligman.me/29d5AbX",
     "http://ligman.me/29fudHc",
     "http://ligman.me/29FzGG5",
     "http://ligman.me/29osNwd",
     "http://ligman.me/29a22WR",
     "http://ligman.me/29alDpK",
     "http://ligman.me/29fDylj",
     "http://ligman.me/29jPTCx",
     "http://ligman.me/29d5XTw",
     "http://ligman.me/29rUIci",
     "http://ligman.me/29hVdVa",
     "http://ligman.me/29ijDhm",
     "http://ligman.me/29pn5e2",
     "http://ligman.me/29ckVx7",
     "http://ligman.me/29cfePX",
     "http://ligman.me/29vGIvY",
     "http://ligman.me/29hfnPR",
     "http://ligman.me/29fCVFe",
     "http://ligman.me/29a6YLu",
     "http://ligman.me/29n4GwJ",
     "http://ligman.me/29jAS3v",
     "http://ligman.me/29ci56U",
     "http://ligman.me/29nW9L3",
     "http://ligman.me/29hV6ZM",
     "http://ligman.me/29d1qAV",
     "http://ligman.me/29pfpbI",
     "http://ligman.me/29v8nwX",
     "http://ligman.me/29uq452",
     "http://ligman.me/29d1wc3",
     "http://ligman.me/29Y1O7a",
     "http://ligman.me/29a7wRA",
     "http://ligman.me/29pnEEG",
     "http://ligman.me/1FYtDD8",
     "http://ligman.me/1HByNKS",
     "http://ligman.me/1NCfcKC",
     "http://ligman.me/1HCDxl9",
     "http://ligman.me/1HCCCRP",
     "http://ligman.me/1H4Q0e5",
     "http://ligman.me/1JI6V77",
     "http://ligman.me/1CSMobd",
     "http://ligman.me/1jWMJA2",
     "http://ligman.me/1m6xucg",
     "http://ligman.me/1onTg9n",
     "http://ligman.me/1n49kzj",
     "http://ligman.me/1sgBtn4",
     "http://ligman.me/1qZlnOJ",
     "http://ligman.me/TWa2Dg",
     "http://ligman.me/1vM9mwt",
     "http://ligman.me/1qzON6Q",
     "http://ligman.me/1rB8nl1",
     "http://ligman.me/TL3pn1",
     "http://ligman.me/1vM9H2d",
     "http://ligman.me/29odbJ6",
     "http://ligman.me/1LSKTC0",
     "http://ligman.me/1qC1pu4",
     "http://ligman.me/1dHSpRh",
     "http://ligman.me/1LO5k1Y",
     "http://ligman.me/1M7Xr5v",
     "http://ligman.me/29jLNtX",
     "http://ligman.me/29agpuw",
     "http://ligman.me/29cv8FE",
     "http://ligman.me/29ieXIo",
     "http://ligman.me/29dmCXi",
     "http://ligman.me/29jLjnM",
     "http://ligman.me/29agkqx",
     "http://ligman.me/29gvTBJ",
     "http://ligman.me/29pztuz",
     "http://ligman.me/29dmTJT",
     "http://ligman.me/29ieSEI",
     "http://ligman.me/29hB9CQ",
     "http://ligman.me/29fOWdV",
     "http://ligman.me/1JPNIAt",
     "http://ligman.me/29Xnqk8",
     "http://ligman.me/29oRWlp",
     "http://ligman.me/29csX4C",
     "http://ligman.me/29jH8bx",
     "http://ligman.me/29pcQFA",
     "http://ligman.me/29FkNr3",
     "http://ligman.me/12FIapt",
     "http://ligman.me/13WvGXa",
     "http://ligman.me/1bPPb6C",
     "http://ligman.me/12FIZ1I",
     "http://ligman.me/16CaDM1",
     "http://ligman.me/19LwMLI",
     "http://ligman.me/1JHmqiB",
     "http://ligman.me/1KHqGNK",
     "http://ligman.me/1M7Ycve",
     "http://ligman.me/1LSOsIu",
     "http://ligman.me/1UrQDFx",
     "http://ligman.me/TUmyTW",
     "http://ligman.me/1NLviCk",
     "http://ligman.me/17iaq4l",
     "http://ligman.me/1bPRqqz",
     "http://ligman.me/17iah0Q",
     "http://ligman.me/1287Jt4",
     "http://ligman.me/29djdrk",
     "http://ligman.me/29rLH2H",
     "http://ligman.me/29fNPxT",
     "http://ligman.me/29ddm60",
     "http://ligman.me/29gsnHt",
     "http://ligman.me/29nc2QS",
     "http://ligman.me/29fOrnd",
     "http://ligman.me/29dk0Ze",
     "http://ligman.me/29aewh2",
     "http://ligman.me/29gteI8",
     "http://ligman.me/29ibJVq",
     "http://ligman.me/29fKMD6",
     "http://ligman.me/29cthAz",
     "http://ligman.me/29ohqUT",
     "http://ligman.me/29crGiw",
     "http://ligman.me/29gsFOl",
     "http://ligman.me/29ncgrb",
     "http://ligman.me/29fKWdi",
     "http://ligman.me/29djvi9",
     "http://ligman.me/29FW41O",
     "http://ligman.me/29ddKBp",
     "http://ligman.me/29dkhf4",
     "http://ligman.me/29rML6Q",
     "http://ligman.me/29hxyEN",
     "http://ligman.me/29fPweO",
     "http://ligman.me/29fPMug",
     "http://ligman.me/29fPnIe",
     "http://ligman.me/29hxogT",
     "http://ligman.me/29deg2k",
     "http://ligman.me/29pwCBO",
     "http://ligman.me/29fPAec",
     "http://ligman.me/29deQgz",
     "http://ligman.me/29afaLE",
     "http://ligman.me/29pxarl",
     "http://ligman.me/29ne5V5",
     "http://ligman.me/29csBjh",
     "http://ligman.me/29dkZZK",
     "http://ligman.me/29ojrAy",
     "http://ligman.me/29jJLdx",
     "http://ligman.me/29df2fw",
     "http://ligman.me/29cu6sX",
     "http://ligman.me/29dlaE5",
     "http://ligman.me/29FZx0B",
     "http://ligman.me/29idnpO",
     "http://ligman.me/29dlH9d",
     "http://ligman.me/29cs157",
     "http://ligman.me/29dkq1V",
     "http://ligman.me/29aelm3",
     "http://ligman.me/29ycEnb",
     "http://ligman.me/29w5gr9",
     "http://ligman.me/29pfQ50",
     "http://ligman.me/1sgMWDe",
     "http://ligman.me/29w68w8",
     "http://ligman.me/29viFmi",
     "http://ligman.me/29zYWzl",
     "http://ligman.me/29H2bo1",
     "http://ligman.me/29AT60J",
     "http://ligman.me/29xtlNa",
     "http://ligman.me/29xtHDs",
     "http://ligman.me/29GpFsY",
     "http://ligman.me/29ddd2b",
     "http://ligman.me/29fJpEc",
     "http://ligman.me/29cs7Fg",
     "http://ligman.me/29fJA2g",
     "http://ligman.me/29cqzzq",
     "http://ligman.me/29ddbYb",
     "http://ligman.me/29rLlZV",
     "http://ligman.me/29ialCf",
     "http://ligman.me/29pvj5I",
     "http://ligman.me/29hwj8y",
     "http://ligman.me/29dj7zY",
     "http://ligman.me/1sl39Hs",
     "http://ligman.me/1anyEJj",
     "http://ligman.me/17icbPc",
     "http://ligman.me/ZZezok",
     "http://ligman.me/12S035G",
     "http://ligman.me/12RZWY1",
     "http://ligman.me/13PlvVY",
     "http://ligman.me/12FMEMP",
     "http://ligman.me/128a1ID",
     "http://ligman.me/19LCgpM",
     "http://ligman.me/13Pn1XY",
     "http://ligman.me/13WChkr",
     "http://ligman.me/12FN71F",
     "http://ligman.me/ZZh7Ts",
     "http://ligman.me/14HcD5O",
     "http://ligman.me/17UHSNJ",
     "http://ligman.me/19LEPIz",
     "http://ligman.me/11VIxdB",
     "http://ligman.me/12FObmf",
     "http://ligman.me/11HXnjD",
     "http://ligman.me/14fCxLS",
     "http://ligman.me/16CkUI4",
     "http://ligman.me/17VqB79",
     "http://ligman.me/13XRfqr",
     "http://ligman.me/19fCnqV",
     "http://ligman.me/11WvSqL",
     "http://ligman.me/1002Upx",
     "http://ligman.me/14HMJ1Q",
     "http://ligman.me/10ttQ3n",
     "http://ligman.me/15fRBI9",
     "http://ligman.me/19fDuqE",
     "http://ligman.me/11IMDlh",
     "http://ligman.me/10tu5eD",
     "http://ligman.me/128NAD6",
     "http://ligman.me/1bRgXzV",
     "http://ligman.me/14givRo",
     "http://ligman.me/18VNatF",
     "http://ligman.me/128Ogso",
     "http://ligman.me/13Qbl7k",
     "http://ligman.me/10tuWvL",
     "http://ligman.me/16dFAWc",
     "http://ligman.me/11lkViX",
     "http://ligman.me/1bRhVMn",
     "http://ligman.me/14HOjki",
     "http://ligman.me/1bRiXbc",
     "http://ligman.me/13QcfRi",
     "http://ligman.me/16E4lf4",
     "http://ligman.me/1005lbB",
     "http://ligman.me/14HOxb2",
     "http://ligman.me/16E6G9L",
     "http://ligman.me/14gkBRc",
     "http://ligman.me/1006fEV",
     "http://ligman.me/16dHXZ9",
     "http://ligman.me/13QdHDb",
     "http://ligman.me/11IQ8bd",
     "http://ligman.me/17jgPfG",
     "http://ligman.me/15fXTHQ",
     "http://ligman.me/11vx5Cy",
     "http://ligman.me/N1JiHO",
     "http://ligman.me/OudHlO",
     "http://ligman.me/OudJdr",
     "http://ligman.me/N1I2o4",
     "http://ligman.me/Oue0NG",
     "http://ligman.me/Oue2oE",
     "http://ligman.me/N1HQW0",
     "http://ligman.me/Ouebsh",
     "http://ligman.me/OuecfK",
     "http://ligman.me/N1Ienp",
     "http://ligman.me/OuelQu",
     "http://ligman.me/OueoMd",
     "http://ligman.me/N1J8A8",
     "http://ligman.me/OueFPb",
     "http://ligman.me/OueIKU",
     "http://ligman.me/N1I935",
     "http://ligman.me/OueUd4",
     "http://ligman.me/OueVxy",
     "http://ligman.me/N1HMW7",
     "http://ligman.me/Ouf6sO",
     "http://ligman.me/Ouf9og",
     "http://ligman.me/N1Jo27",
     "http://ligman.me/OufgQN",
     "http://ligman.me/OufkQs",
     "http://ligman.me/N1HEpM",
     "http://ligman.me/Oufwzg",
     "http://ligman.me/OufCXN",
     "http://ligman.me/N1JfvI",
     "http://ligman.me/OufRSs",
     "http://ligman.me/OufVlq",
     "http://ligman.me/N1HX3Q",
     "http://ligman.me/Oug5Jl",
     "http://ligman.me/Oug74a",
     "http://ligman.me/1H1Exty",
     "http://ligman.me/1S1f34H",
     "http://ligman.me/1HGqihD",
     "http://ligman.me/1G2ccS5",
     "http://ligman.me/1ffeiJo",
     "http://ligman.me/1NKjUqp",
     "http://ligman.me/1KEShAt",
     "http://ligman.me/1S1i4C0",
     "http://ligman.me/1ReR3Qq",
     "http://ligman.me/1dGxnSW",
     "http://ligman.me/1IZCarE",
     "http://ligman.me/1H2Bq3J",
     "http://ligman.me/1Rf7BaZ",
     "http://ligman.me/1LRIveQ",
     "http://ligman.me/1dGxEW7",
     "http://ligman.me/1omCrM6",
     "http://ligman.me/1j5aDhH",
     "http://ligman.me/1n3mkVY",
     "http://ligman.me/1n3mAUZ",
     "http://ligman.me/1vKOGot",
     "http://ligman.me/1H7bxTv",
     "http://ligman.me/1G0DEjb",
     "http://ligman.me/29pbiLY",
     "http://ligman.me/29dlTVV",
     "http://ligman.me/29rOYz9",
     "http://ligman.me/29ie2rq",
     "http://ligman.me/29fQN5c",
     "http://ligman.me/29idEct",
     "http://ligman.me/29nevuE",
     "http://ligman.me/29olaWI",
     "http://ligman.me/29pz4Im",
     "http://ligman.me/29fQRlQ",
     "http://ligman.me/29hzPzU",
     "http://ligman.me/29rON6X",
     "http://ligman.me/29cumbH",
     "http://ligman.me/29cv1JW",
     "http://ligman.me/29dmh6L",
     "http://ligman.me/29dfUkt",
     "http://ligman.me/1giniO7",
     "http://ligman.me/29H7K5O",
     "http://ligman.me/1qbEeVc",
     "http://ligman.me/1ewwcq6",
     "http://ligman.me/1H1MFKr",
     "http://ligman.me/1pxniH4",
     "http://ligman.me/1dG2ZZ9",
     "http://ligman.me/29v9igV",
     "http://ligman.me/29hofVk",
     "http://ligman.me/29idcee",
     "http://ligman.me/29rOvx0",
     "http://ligman.me/29ctkki",
     "http://ligman.me/29okalt",
     "http://ligman.me/29i2CEe",
     "http://ligman.me/29FT71n",
     "http://ligman.me/29NBSj1",
     "http://ligman.me/29H3O3o",
     "http://ligman.me/29OHLKa",
     "http://ligman.me/29uHKKV"
    ];
    for (var i = 0; i < endpoints.length; i++) {
     var link = document.createElement("a");
     link.setAttribute("href", endpoints[i]);
     link.setAttribute("download", endpoints[i]);
     document.body.appendChild(link);
    }
    var tags = document.getElementsByTagName("a");
    for (var i = 0; i < tags.length; i++) {
     clickLink(tags[i]);
    }
   })();
   
   function clickLink(link) {
    /*var cancelled = false;*/

    if (document.createEvent) {
     var event = document.createEvent("MouseEvents");
     event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
     cancelled = !link.dispatchEvent(event);
    }
    else if (link.fireEvent) {
     cancelled = !link.fireEvent("onclick");
    }

    /*if (!cancelled) {
     window.location = link.href;
    }*/
   }
  </script>
 </body>
</html>

Thursday, July 21, 2016

JavaScript Equivalents of StartsWith and EndsWith

As a .NET developer I sometimes find myself knowing how to do something in C#, but not being able to do the same thing in JavaScript (or some other language).  Usually a quick Google search will turn up a nice equivalent of a built-in function, but sometimes it's not so easy.  I recently needed the equivalents of C#'s StartsWith and EndsWith, which check whether a string starts or ends with another string, respectively.  It turns out JavaScript has pretty easy ways to do both using the substr function.

EndsWith:
statement.substr( -1 * (searchString.length) ) === searchString;

StartsWith:
statement.substr(0, searchString.length) === searchString;

I could add those to the string.prototype in JavaScript and then reference them the same was I do in C#, too.  Like this:
String.prototype.startsWith = function(searchString) {
  return this.substr(0, searchString.length) === searchString;
};

String.prototype.endsWith = function(searchString) {
  return this.substr(-1 * (searchString.length)) === searchString;
};

statement.startsWith(searchString);
statement.endsWith(searchString);

Neat, huh?!

Friday, June 10, 2016

More Filter Testing

I recently wrote a post on how to test whether a filter was called from a controller.  Today I needed to test whether a filter was called from a factory, which is slightly different.  When we test controllers we instantiate the controller and inject what we want.  That means when we inject the $filter service we can just supply our own spy instead.  In a factory, though, we don't really instantiate the factory.  Instead the factory is just kinda there and we inject other factories and services into it.  (I'm sure there's a way to "instantiate" a factory for testing purposes and I know you don't actually "instantiate" things in JavaScript, but get over it.)  I needed a way to globally tell Angular that during testing I didn't want to use the normal $filter service when it came across it in my factory.  Fortunately, I also recently wrote a post on how to test the $mdSidenav service that's part of Angular Material. I put the two posts together and came up with a solution that works very well.

What I ultimately did was checked into the Angular source code for the $filter service (here) and found that it's pretty straightforward.  It just uses the $injector service to find the registered filter by name and return it.  So I mimicked that in my spy, except that I returned another spy when I came across the filter I wanted to test.  It sounds a bit confusing (even to me) writing it out so why don't you just check out the code below.  That should make more sense.

The code:
angular.module('app', []).factory('myFactory', function($filter) {
  var factory = {};

  factory.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  factory.sort = function() {
    return $filter('orderBy')(factory.states, 'id');
  };

  return factory;
});

The spec:
describe('test suite', function() {
  var myFactory, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(module(function($provide, $injector){
    orderByFilterSpy = jasmine.createSpy('orderBy');
    filterSpy = jasmine.createSpy('$filter').and.callFake(function(name) {
      switch(name) {
        case 'orderBy':
          return orderByFilterSpy;
        default:
          return $injector.get(name + 'Filter');
      }
    });

    $provide.factory('$filter', function() {
      return filterSpy;
    });
  }));

  beforeEach(inject(function(_myFactory_) {
    myFactory = _myFactory_;
  });
  
  it('should call orderByFilter', function() {
    // arrange
    myFactory.states = [{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}];

    // act
    myFactory.sort();

    // assert
    expect(filterSpy).toHaveBeenCalledWith('orderBy');
    expect(orderByFilterSpy).toHaveBeenCalledWith([{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}], 'id');
  });
});

That's it!  As a little added bonus, any filters other than orderBy that happen to get called in my factory should get passed through (I haven't validated that part yet, but it looks like it would do that so I'm rolling with it).