From 65bfb7b60975ae0478c9c93242f0262dde85e3dd Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sun, 26 Jan 2014 01:50:17 +0800 Subject: [PATCH] feat(uiSrefActive): nested state and DOM decendant support for ui-sref-active uiSrefActive now takes all decendant uiSrefs into consideration. uiSrefActive also adds a class with "-nested" postfix when a child state of the related uiSrefs becomes active. Closes #704. --- src/stateDirectives.js | 89 +++++++++++++++++++++++-------------- test/stateDirectivesSpec.js | 87 ++++++++++++++++++++++++++++++++---- 2 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/stateDirectives.js b/src/stateDirectives.js index 039b213b2..4e75b51c1 100644 --- a/src/stateDirectives.js +++ b/src/stateDirectives.js @@ -17,6 +17,7 @@ function stateContext(el) { * @name ui.router.state.directive:ui-sref * * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams * @requires $timeout * * @restrict A @@ -48,14 +49,13 @@ function stateContext(el) { * * @param {string} ui-sref 'stateName' can be any valid absolute or relative state */ -$StateRefDirective.$inject = ['$state', '$timeout']; -function $StateRefDirective($state, $timeout) { +$StateRefDirective.$inject = ['$state', '$timeout', '$stateParams']; +function $StateRefDirective($state, $timeout, $stateParams) { return { restrict: 'A', - require: '?^uiSrefActive', - link: function(scope, element, attrs, uiSrefActive) { + link: function(scope, element, attrs) { var ref = parseStateRef(attrs.uiSref); - var params = null, url = null, base = stateContext(element) || $state.$current; + var state = null, params = null, url = null, base = stateContext(element) || $state.$current; var isForm = element[0].nodeName === "FORM"; var attr = isForm ? "action" : "href", nav = true; @@ -64,10 +64,8 @@ function $StateRefDirective($state, $timeout) { if (!nav) return; var newHref = $state.href(ref.state, params, { relative: base }); + state = $state.get(ref.state, base); - if (uiSrefActive) { - uiSrefActive.$$setStateInfo(ref.state, params); - } if (!newHref) { nav = false; return false; @@ -95,6 +93,37 @@ function $StateRefDirective($state, $timeout) { e.preventDefault(); } }); + + var emitEvents = function(){ + // HACK: + // Emits events only after + // 1. The execution of link functions of ancestor's ui-sref-active + // or, + // 2. The ancestor ui-sref-active has removed their previously appended classes. + // + $timeout(function(){ + if($state.$current.self === state && matchesParams()){ + // Exact match of current state + scope.$emit('$uiSrefActivated'); + }else if($state.includes(state.name) && matchesParams()){ + // The current state is a child of reference state + scope.$emit('$uiSrefChildStateActivated'); + } + }); + }; + + // Emits $uiSref*Activated events. + scope.$on('$stateChangeSuccess', emitEvents); + + // Also emits the events when the element is first created (linked). + // This makes sure the events are emitted if a state is directly navigated + // through the browser navigation bar. + // + emitEvents(); + + function matchesParams() { + return !params || equalForKeys(params, $stateParams); + } } }; } @@ -104,7 +133,6 @@ function $StateRefDirective($state, $timeout) { * @name ui.router.state.directive:ui-sref-active * * @requires ui.router.state.$state - * @requires ui.router.state.$stateParams * @requires $interpolate * * @restrict A @@ -126,38 +154,33 @@ function $StateRefDirective($state, $timeout) { * * */ -$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; -function $StateActiveDirective($state, $stateParams, $interpolate) { +$StateActiveDirective.$inject = ['$state', '$interpolate']; +function $StateActiveDirective($state, $interpolate) { return { restrict: "A", - controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { - var state, params, activeClass; + scope: true, // Catching $uiSref*Activated events without sibling's interferance. + link: function(scope, element, attrs) { + var activeClass, activeClassNested, activeClassList; // There probably isn't much point in $observing this - activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope); + activeClass = $interpolate(attrs.uiSrefActive || '', false)(scope); + activeClassNested = activeClass + '-nested'; + activeClassList = [activeClass, activeClassNested].join(' '); // space-separated list of all appended classes - // Allow uiSref to communicate with uiSrefActive - this.$$setStateInfo = function(newState, newParams) { - state = $state.get(newState, stateContext($element)); - params = newParams; - update(); - }; + // Remove all previously appended classes. + scope.$on('$stateChangeSuccess', function(){ + element.removeClass(activeClassList); + }); - $scope.$on('$stateChangeSuccess', update); + scope.$on('$uiSrefActivated', function(){ + element.addClass(activeClass); + }); - // Update route state - function update() { - if ($state.$current.self === state && matchesParams()) { - $element.addClass(activeClass); - } else { - $element.removeClass(activeClass); - } - } + scope.$on('$uiSrefChildStateActivated', function(){ + element.addClass(activeClassNested); + }); - function matchesParams() { - return !params || equalForKeys(params, $stateParams); - } - }] + } }; } diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index f4e10a012..874467472 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -278,6 +278,8 @@ describe('uiSrefActive', function() { url: '/:id', }).state('contacts.item.detail', { url: '/detail/:foo' + }).state('contacts.item.edit', { + url: '/edit' }); })); @@ -285,51 +287,118 @@ 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('
Contacts
'); template = $compile(el)($rootScope); $rootScope.$digest(); + $timeout.flush(); // emitEvent timeout hacks - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); + $state.transitionTo('contacts'); + $timeout.flush(); // emitEvent timeout hacks + $q.flush(); + + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active'); + + $state.transitionTo('contacts.item', { id: 5 }); + $timeout.flush(); // emitEvent timeout hacks + $q.flush(); + + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active-nested'); + })); + + it('should update class for decendant uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) { + el = angular.element('

Contacts

'); + template = $compile(el)($rootScope); + $rootScope.$digest(); + $timeout.flush(); // emitEvent timeout hacks + + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope'); $state.transitionTo('contacts'); + $timeout.flush(); // emitEvent timeout hacks $q.flush(); - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope active'); $state.transitionTo('contacts.item', { id: 5 }); + $timeout.flush(); // emitEvent timeout hacks $q.flush(); - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); + + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope active-nested'); })); - it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) { + it('should not update class for sibling elements with uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) { + el = angular.element('
Contacts
'); + template = $compile(el)($rootScope); + $rootScope.$digest(); + $timeout.flush(); // emitEvent timeout hacks + + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope'); + $state.transitionTo('contacts'); + $timeout.flush(); // emitEvent timeout hacks + $q.flush(); + + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope'); + + $state.transitionTo('contacts.item', { id: 5 }); + $timeout.flush(); // emitEvent timeout hacks + $q.flush(); + + expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope'); + })); + + it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state, $timeout) { el = angular.element('
Contacts
'); template = $compile(el)($rootScope); $rootScope.$digest(); + $timeout.flush(); // emitEvent timeout hacks - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); $state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' }); $q.flush(); - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); + $timeout.flush(); // emitEvent timeout hacks + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active'); $state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' }); $q.flush(); - expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); + $timeout.flush(); // emitEvent timeout hacks + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); + })); + + it('should match child states', inject(function($rootScope, $q, $compile, $state, $timeout) { + el = angular.element('
Contacts
'); + template = $compile(el)($rootScope); + $rootScope.$digest(); + $timeout.flush(); // emitEvent timeout hacks + + $state.transitionTo('contacts.item.edit', { id: 1 }); + $q.flush(); + $timeout.flush(); // emitEvent timeout hacks + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active-nested'); + + $state.transitionTo('contacts.item.edit', { id: 4 }); + $q.flush(); + $timeout.flush(); // emitEvent timeout hacks + expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); })); - 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('
'); template = $compile(el)($rootScope); $rootScope.$digest(); $state.transitionTo('contacts'); + $timeout.flush(); // emitEvent timeout hacks $q.flush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); $state.transitionTo('contacts.item', { id: 6 }); + $timeout.flush(); // emitEvent timeout hacks $q.flush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active'); $state.transitionTo('contacts.item', { id: 5 }); + $timeout.flush(); // emitEvent timeout hacks $q.flush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope'); }));