diff --git a/src/stateDirectives.ts b/src/stateDirectives.ts index 11789e43e..15f07fcda 100644 --- a/src/stateDirectives.ts +++ b/src/stateDirectives.ts @@ -222,13 +222,14 @@ $StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; function $StateRefActiveDirective($state, $stateParams, $interpolate) { return { restrict: "A", - controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { - var states = [], activeClass; + controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) { + 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) { @@ -246,27 +247,18 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) { // Update route state function update() { - if (anyMatch()) { - $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 (anyMatch(states[i].state, states[i].params)) { + addClass($element, activeClass); + } else { + removeClass($element, activeClass); } - } - return false; - } - function isMatch(state, params) { - if (typeof $attrs.uiSrefActiveEq !== 'undefined') { - return $state.is(state.name, params); - } else { - return $state.includes(state.name, params); + if (exactMatch(states[i].state, states[i].params)) { + addClass($element, activeEqClass); + } else { + removeClass($element, activeEqClass); + } } return $state.includes(state.name) && matchesParams(); } @@ -274,6 +266,15 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) { function matchesParams() { return !params || equalForKeys(params, $stateParams); } + + 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 6ab606171..9b6ff5634 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -426,8 +426,16 @@ describe('uiSrefActive', function() { }); })); - beforeEach(inject(function($document) { + beforeEach(inject(function($document, $timeout) { document = $document[0]; + timeoutFlush = function () { + try { + $timeout.flush(); + } catch (e) { + // Angular 1.0.8 throws 'No deferred tasks to be flushed' if there is nothing in queue. + // Behave as Angular >=1.1.5 and do nothing in such case. + } + } })); it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) { @@ -438,11 +446,12 @@ describe('uiSrefActive', function() { expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); - + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); $state.transitionTo('contacts.item', { id: 2 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); })); @@ -454,10 +463,12 @@ describe('uiSrefActive', function() { expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); $state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); $state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); })); @@ -468,10 +479,12 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item.edit', { id: 1 }); $q.flush(); + timeoutFlush(); expect(a.attr('class')).toMatch(/active/); $state.transitionTo('contacts.item.edit', { id: 4 }); $q.flush(); + timeoutFlush(); expect(a.attr('class')).not.toMatch(/active/); })); @@ -482,13 +495,33 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); + timeoutFlush(); expect(a.attr('class')).toMatch(/active/); $state.transitionTo('contacts.item.edit', { id: 1 }); $q.flush(); + timeoutFlush(); 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, $timeout) { + template = $compile('
Contacts
')($rootScope); + $rootScope.$digest(); + var a = angular.element(template[0].getElementsByTagName('a')[0]); + + $state.transitionTo('contacts.item', { id: 1 }); + $q.flush(); + timeoutFlush(); + expect(a.attr('class')).toMatch(/active/); + expect(a.attr('class')).toMatch(/active-eq/); + + $state.transitionTo('contacts.item.edit', { id: 1 }); + $q.flush(); + timeoutFlush(); + 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('
'); template = $compile(el)($rootScope); @@ -496,14 +529,17 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts'); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); $state.transitionTo('contacts.item', { id: 6 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active'); $state.transitionTo('contacts.item', { id: 5 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); })); @@ -516,10 +552,12 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0]).attr('class')).toBe('ng-scope active'); $state.transitionTo('contacts.item', { id: 2 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0]).attr('class')).toBe('ng-scope active'); })); @@ -534,10 +572,12 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); $state.transitionTo('contacts.lazy'); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); })); @@ -552,10 +592,12 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); $state.transitionTo('contacts.lazy'); $q.flush(); + timeoutFlush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); })); });