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'); }));