From f6ea875109babf97b0d18711f44c9fa7038bb18c Mon Sep 17 00:00:00 2001
From: Joshua Higgins <joshua.higgins@sungard.com>
Date: Thu, 28 May 2015 15:26:21 -0400
Subject: [PATCH 1/2] feat(uiSrefActive): allow active & active-eq on same
 element

---
 src/stateDirectives.js      | 41 ++++++++++++++++++-------------------
 test/stateDirectivesSpec.js | 16 +++++++++++++++
 2 files changed, 36 insertions(+), 21 deletions(-)

diff --git a/src/stateDirectives.js b/src/stateDirectives.js
index 09991030c..6b7ac0a5e 100644
--- a/src/stateDirectives.js
+++ b/src/stateDirectives.js
@@ -229,12 +229,13 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
   return  {
     restrict: "A",
     controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
-      var states = [], activeClass;
+      var states = [], activeClass, activeEqClass;
 
       // There probably isn't much point in $observing this
       // uiSrefActive and uiSrefActiveEq share the same directive object with some
       // slight difference in logic routing
-      activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope);
+      activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
+      activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope);
 
       // Allow uiSref to communicate with uiSrefActive[Equals]
       this.$$addStateInfo = function (newState, newParams) {
@@ -252,29 +253,27 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
 
       // Update route state
       function update() {
-        if (anyMatch()) {
-          $element.addClass(activeClass);
-        } else {
-          $element.removeClass(activeClass);
-        }
-      }
+        if (states.length) {
+          for (var i = 0; i < states.length; i++) {
+            if (anyMatch(states[i].state, states[i].params)) {
+              $element.addClass(activeClass);
+            } else {
+              $element.removeClass(activeClass);
+            }
 
-      function anyMatch() {
-        for (var i = 0; i < states.length; i++) {
-          if (isMatch(states[i].state, states[i].params)) {
-            return true;
-          }
+            if (exactMatch(states[i].state, states[i].params)) {
+              $element.addClass(activeEqClass);
+            } else {
+              $element.removeClass(activeEqClass);
+            }
+          }          
         }
-        return false;
       }
 
-      function isMatch(state, params) {
-        if (typeof $attrs.uiSrefActiveEq !== 'undefined') {
-          return $state.is(state.name, params);
-        } else {
-          return $state.includes(state.name, params);
-        }
-      }
+      function anyMatch(state, params) { return $state.includes(state.name, params); }
+
+      function exactMatch(state, params) { return $state.is(state.name, params); }
+
     }]
   };
 }
diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js
index c9b621b7d..26a48b8ec 100644
--- a/test/stateDirectivesSpec.js
+++ b/test/stateDirectivesSpec.js
@@ -479,6 +479,22 @@ describe('uiSrefActive', function() {
     expect(a.attr('class')).not.toMatch(/active/);
   }));
 
+  it('should match on child states when active-equals and active-equals-eq is used', inject(function($rootScope, $q, $compile, $state) {
+    template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active" ui-sref-active-eq="active-eq">Contacts</a></div>')($rootScope);
+    $rootScope.$digest();
+    var a = angular.element(template[0].getElementsByTagName('a')[0]);
+
+    $state.transitionTo('contacts.item', { id: 1 });
+    $q.flush();
+    expect(a.attr('class')).toMatch(/active/);
+    expect(a.attr('class')).toMatch(/active-eq/);
+
+    $state.transitionTo('contacts.item.edit', { id: 1 });
+    $q.flush();
+    expect(a.attr('class')).toMatch(/active/);
+    expect(a.attr('class')).not.toMatch(/active-eq/);
+  }));
+
   it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
     el = angular.element('<section><div ui-view></div></section>');
     template = $compile(el)($rootScope);

From 085fdf2fb18b00ae03c3301173a00edfa76174a9 Mon Sep 17 00:00:00 2001
From: Joshua Higgins <joshua.higgins@sungard.com>
Date: Thu, 28 May 2015 17:35:22 -0400
Subject: [PATCH 2/2] fix(uiSrefActive): add $timeout to prevent DOM race

closes #1997
---
 src/stateDirectives.js      | 30 ++++++++++++++++--------------
 test/stateDirectivesSpec.js | 37 ++++++++++++++++++++++++++++---------
 2 files changed, 44 insertions(+), 23 deletions(-)

diff --git a/src/stateDirectives.js b/src/stateDirectives.js
index 6b7ac0a5e..8f23b1fdb 100644
--- a/src/stateDirectives.js
+++ b/src/stateDirectives.js
@@ -228,7 +228,7 @@ $StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
 function $StateRefActiveDirective($state, $stateParams, $interpolate) {
   return  {
     restrict: "A",
-    controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
+    controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) {
       var states = [], activeClass, activeEqClass;
 
       // There probably isn't much point in $observing this
@@ -253,23 +253,25 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
 
       // Update route state
       function update() {
-        if (states.length) {
-          for (var i = 0; i < states.length; i++) {
-            if (anyMatch(states[i].state, states[i].params)) {
-              $element.addClass(activeClass);
-            } else {
-              $element.removeClass(activeClass);
-            }
+        for (var i = 0; i < states.length; i++) {
+          if (anyMatch(states[i].state, states[i].params)) {
+            addClass($element, activeClass);
+          } else {
+            removeClass($element, activeClass);
+          }
 
-            if (exactMatch(states[i].state, states[i].params)) {
-              $element.addClass(activeEqClass);
-            } else {
-              $element.removeClass(activeEqClass);
-            }
-          }          
+          if (exactMatch(states[i].state, states[i].params)) {
+            addClass($element, activeEqClass);
+          } else {
+            removeClass($element, activeEqClass);
+          }
         }
       }
 
+      function addClass(el, className) { $timeout(function() { el.addClass(className); }); }
+
+      function removeClass(el, className) { el.removeClass(className); }
+
       function anyMatch(state, params) { return $state.includes(state.name, params); }
 
       function exactMatch(state, params) { return $state.is(state.name, params); }
diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js
index 26a48b8ec..4e8a5c422 100644
--- a/test/stateDirectivesSpec.js
+++ b/test/stateDirectivesSpec.js
@@ -420,100 +420,113 @@ describe('uiSrefActive', function() {
     document = $document[0];
   }));
 
-  it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) {
+  it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a><a ui-sref="contacts.item({ id: 2 })" ui-sref-active="active">Contacts</a></div>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
 
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
 
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
 
     $state.transitionTo('contacts.item', { id: 2 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
   }));
 
-  it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) {
+  it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<div><a ui-sref="contacts.item.detail({ foo: \'bar\' })" ui-sref-active="active">Contacts</a></div>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
 
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
     $state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
 
     $state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
   }));
 
-  it('should match on child states', inject(function($rootScope, $q, $compile, $state) {
+  it('should match on child states', inject(function($rootScope, $q, $compile, $state, $timeout) {
     template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></div>')($rootScope);
     $rootScope.$digest();
     var a = angular.element(template[0].getElementsByTagName('a')[0]);
 
     $state.transitionTo('contacts.item.edit', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).toMatch(/active/);
 
     $state.transitionTo('contacts.item.edit', { id: 4 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).not.toMatch(/active/);
   }));
 
-  it('should NOT match on child states when active-equals is used', inject(function($rootScope, $q, $compile, $state) {
+  it('should NOT match on child states when active-equals is used', inject(function($rootScope, $q, $compile, $state, $timeout) {
     template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active-eq="active">Contacts</a></div>')($rootScope);
     $rootScope.$digest();
     var a = angular.element(template[0].getElementsByTagName('a')[0]);
 
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).toMatch(/active/);
 
     $state.transitionTo('contacts.item.edit', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).not.toMatch(/active/);
   }));
 
-  it('should match on child states when active-equals and active-equals-eq is used', inject(function($rootScope, $q, $compile, $state) {
+  it('should match on child states when active-equals and active-equals-eq is used', inject(function($rootScope, $q, $compile, $state, $timeout) {
     template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active" ui-sref-active-eq="active-eq">Contacts</a></div>')($rootScope);
     $rootScope.$digest();
     var a = angular.element(template[0].getElementsByTagName('a')[0]);
 
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).toMatch(/active/);
     expect(a.attr('class')).toMatch(/active-eq/);
 
     $state.transitionTo('contacts.item.edit', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(a.attr('class')).toMatch(/active/);
     expect(a.attr('class')).not.toMatch(/active-eq/);
   }));
 
-  it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
+  it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<section><div ui-view></div></section>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
 
     $state.transitionTo('contacts');
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
 
     $state.transitionTo('contacts.item', { id: 6 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');
 
     $state.transitionTo('contacts.item', { id: 5 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
   }));
 
-  it('should match on any child state refs', inject(function($rootScope, $q, $compile, $state) {
+  it('should match on any child state refs', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<div ui-sref-active="active"><a ui-sref="contacts.item({ id: 1 })">Contacts</a><a ui-sref="contacts.item({ id: 2 })">Contacts</a></div>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
@@ -521,15 +534,17 @@ describe('uiSrefActive', function() {
     expect(angular.element(template[0]).attr('class')).toBe('ng-scope');
 
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0]).attr('class')).toBe('ng-scope active');
 
     $state.transitionTo('contacts.item', { id: 2 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0]).attr('class')).toBe('ng-scope active');
   }));
 
-  it('should match fuzzy on lazy loaded states', inject(function($rootScope, $q, $compile, $state) {
+  it('should match fuzzy on lazy loaded states', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<div><a ui-sref="contacts.lazy" ui-sref-active="active">Lazy Contact</a></div>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
@@ -539,15 +554,17 @@ describe('uiSrefActive', function() {
     });
 
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
 
     $state.transitionTo('contacts.lazy');
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
   }));
 
-  it('should match exactly on lazy loaded states', inject(function($rootScope, $q, $compile, $state) {
+  it('should match exactly on lazy loaded states', inject(function($rootScope, $q, $compile, $state, $timeout) {
     el = angular.element('<div><a ui-sref="contacts.lazy" ui-sref-active-eq="active">Lazy Contact</a></div>');
     template = $compile(el)($rootScope);
     $rootScope.$digest();
@@ -557,10 +574,12 @@ describe('uiSrefActive', function() {
     });
 
     $state.transitionTo('contacts.item', { id: 1 });
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
 
     $state.transitionTo('contacts.lazy');
+    $timeout.flush();
     $q.flush();
     expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
   }));