From d9a676ba2c4d9e954be224c60496bcb38f6074e3 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Wed, 3 Jun 2015 20:32:30 -0400 Subject: [PATCH] feat(uiSrefActive): allow active & active-eq on same element closes #1997 --- src/stateDirectives.js | 43 +++++++++++++++++----------------- test/stateDirectivesSpec.js | 46 +++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/stateDirectives.js b/src/stateDirectives.js index 09991030c..8835ee91a 100644 --- a/src/stateDirectives.js +++ b/src/stateDirectives.js @@ -228,13 +228,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) { @@ -252,29 +253,29 @@ 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); + } } } + + 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 c9b621b7d..9f696cff9 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -416,8 +416,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) { @@ -428,11 +436,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(''); })); @@ -444,10 +453,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(''); })); @@ -458,10 +469,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/); })); @@ -472,13 +485,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); @@ -486,14 +519,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'); })); @@ -506,10 +542,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'); })); @@ -524,10 +562,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'); })); @@ -542,10 +582,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'); })); });