From eec4932ec5ecb99ad2e02471e46cc860c2435f94 Mon Sep 17 00:00:00 2001 From: Tim Kindberg Date: Mon, 3 Mar 2014 19:56:46 -0500 Subject: [PATCH] feat(uiSrefActive): Also activate for child states. To limit activation to target state use new ui-sref-active-equals directive' Breaking Change: Since ui-sref-active now activates even when child states are active you may need to swap out your ui-sref-active with ui-sref-active-equals, thought typically we think devs want the auto inheritance. Fixes #818 --- src/stateDirectives.js | 76 +++++++++++++++++++++++++++---------- test/stateDirectivesSpec.js | 36 ++++++++++++++++-- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/stateDirectives.js b/src/stateDirectives.js index 2dd89627a..2280ac9b5 100644 --- a/src/stateDirectives.js +++ b/src/stateDirectives.js @@ -80,7 +80,7 @@ function $StateRefDirective($state, $timeout) { return { restrict: 'A', - require: '?^uiSrefActive', + require: ['?^uiSrefActive', '?^uiSrefActiveEquals'], link: function(scope, element, attrs, uiSrefActive) { var ref = parseStateRef(attrs.uiSref); var params = null, url = null, base = stateContext(element) || $state.$current; @@ -103,8 +103,9 @@ function $StateRefDirective($state, $timeout) { var newHref = $state.href(ref.state, params, options); - if (uiSrefActive) { - uiSrefActive.$$setStateInfo(ref.state, params); + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; + if (activeDirective) { + activeDirective.$$setStateInfo(ref.state, params); } if (!newHref) { nav = false; @@ -148,12 +149,19 @@ function $StateRefDirective($state, $timeout) { * @restrict A * * @description - * A directive working alongside ui-sref to add classes to an element when the + * A directive working alongside ui-sref to add classes to an element when the * related ui-sref directive's state is active, and removing them when it is inactive. - * The primary use-case is to simplify the special appearance of navigation menus + * The primary use-case is to simplify the special appearance of navigation menus * relying on `ui-sref`, by having the "active" state's menu button appear different, * distinguishing it from the inactive menu items. * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * one found will be used. + * + * Only activates when the ui-sref's actual target state is active, *not* any of it's children. + * For adding a class based on active children use + * {@link ui.router.state.directive:ui-sref-active-equals ui-sref-active-equals} + * * @example * Given the following template: *
@@ -163,8 +171,8 @@ function $StateRefDirective($state, $timeout) {
  *   
  * 
  * 
- * - * When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins", + * + * When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins", * the resulting HTML will appear as (note the 'active' class): *
  * 
  * 
- * - * The class name is interpolated **once** during the directives link time (any further changes to the - * interpolated value are ignored). - * + * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * * Multiple classes may be specified in a space-separated format: *
  * 
  * 
*/ -$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; -function $StateActiveDirective($state, $stateParams, $interpolate) { - return { + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-equals + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will also activate + * when any descendant state of the ui-sref's target is active. It uses `$state.includes(uiSrefState)` + * to check for activation. + * + */ +$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', function ($scope, $element, $attrs) { var state, params, activeClass; // There probably isn't much point in $observing this - activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope); + // uiSrefActive and uiSrefActiveEquals share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate($attrs.uiSrefActiveEquals || $attrs.uiSrefActive || '', false)($scope); - // Allow uiSref to communicate with uiSrefActive - this.$$setStateInfo = function(newState, newParams) { + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$setStateInfo = function (newState, newParams) { state = $state.get(newState, stateContext($element)); params = newParams; update(); @@ -207,13 +234,21 @@ function $StateActiveDirective($state, $stateParams, $interpolate) { // Update route state function update() { - if ($state.$current.self === state && matchesParams()) { + if (isMatch()) { $element.addClass(activeClass); } else { $element.removeClass(activeClass); } } + function isMatch() { + if (typeof $attrs.uiSrefActiveEquals !== 'undefined') { + return $state.$current.self === state && matchesParams(); + } else { + return $state.includes(state.name) && matchesParams(); + } + } + function matchesParams() { return !params || equalForKeys(params, $stateParams); } @@ -223,4 +258,5 @@ function $StateActiveDirective($state, $stateParams, $interpolate) { angular.module('ui.router.state') .directive('uiSref', $StateRefDirective) - .directive('uiSrefActive', $StateActiveDirective); + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEquals', $StateRefActiveDirective); diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index 5a53eae26..3ee7ec36d 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -304,6 +304,8 @@ describe('uiSrefActive', function() { url: '/:id', }).state('contacts.item.detail', { url: '/detail/:foo' + }).state('contacts.item.edit', { + url: '/edit' }); })); @@ -312,17 +314,17 @@ describe('uiSrefActive', function() { })); it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) { - el = angular.element('
Contacts
'); + el = angular.element('
ContactsContacts
'); template = $compile(el)($rootScope); $rootScope.$digest(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); - $state.transitionTo('contacts'); + $state.transitionTo('contacts.item', { id: 1 }); $q.flush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active'); - $state.transitionTo('contacts.item', { id: 5 }); + $state.transitionTo('contacts.item', { id: 2 }); $q.flush(); expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); })); @@ -342,6 +344,34 @@ describe('uiSrefActive', function() { expect(angular.element(template[0].querySelector('a')).attr('class')).toBe(''); })); + it('should match on child states', inject(function($rootScope, $q, $compile, $state) { + template = $compile('
Contacts
')($rootScope); + $rootScope.$digest(); + var a = angular.element(template[0].getElementsByTagName('a')[0]); + + $state.transitionTo('contacts.item.edit', { id: 1 }); + $q.flush(); + expect(a.attr('class')).toMatch(/active/); + + $state.transitionTo('contacts.item.edit', { id: 4 }); + $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) { + template = $compile('
Contacts
')($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/); + + $state.transitionTo('contacts.item.edit', { id: 1 }); + $q.flush(); + expect(a.attr('class')).not.toMatch(/active/); + })); + it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) { el = angular.element('
'); template = $compile(el)($rootScope);