Skip to content

Commit eec4932

Browse files
committed
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
1 parent cb1c9cf commit eec4932

File tree

2 files changed

+89
-23
lines changed

2 files changed

+89
-23
lines changed

src/stateDirectives.js

+56-20
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function $StateRefDirective($state, $timeout) {
8080

8181
return {
8282
restrict: 'A',
83-
require: '?^uiSrefActive',
83+
require: ['?^uiSrefActive', '?^uiSrefActiveEquals'],
8484
link: function(scope, element, attrs, uiSrefActive) {
8585
var ref = parseStateRef(attrs.uiSref);
8686
var params = null, url = null, base = stateContext(element) || $state.$current;
@@ -103,8 +103,9 @@ function $StateRefDirective($state, $timeout) {
103103

104104
var newHref = $state.href(ref.state, params, options);
105105

106-
if (uiSrefActive) {
107-
uiSrefActive.$$setStateInfo(ref.state, params);
106+
var activeDirective = uiSrefActive[1] || uiSrefActive[0];
107+
if (activeDirective) {
108+
activeDirective.$$setStateInfo(ref.state, params);
108109
}
109110
if (!newHref) {
110111
nav = false;
@@ -148,12 +149,19 @@ function $StateRefDirective($state, $timeout) {
148149
* @restrict A
149150
*
150151
* @description
151-
* A directive working alongside ui-sref to add classes to an element when the
152+
* A directive working alongside ui-sref to add classes to an element when the
152153
* related ui-sref directive's state is active, and removing them when it is inactive.
153-
* The primary use-case is to simplify the special appearance of navigation menus
154+
* The primary use-case is to simplify the special appearance of navigation menus
154155
* relying on `ui-sref`, by having the "active" state's menu button appear different,
155156
* distinguishing it from the inactive menu items.
156157
*
158+
* ui-sref-active can live on the same element as ui-sref or on a parent element. The first
159+
* one found will be used.
160+
*
161+
* Only activates when the ui-sref's actual target state is active, *not* any of it's children.
162+
* For adding a class based on active children use
163+
* {@link ui.router.state.directive:ui-sref-active-equals ui-sref-active-equals}
164+
*
157165
* @example
158166
* Given the following template:
159167
* <pre>
@@ -163,8 +171,8 @@ function $StateRefDirective($state, $timeout) {
163171
* </li>
164172
* </ul>
165173
* </pre>
166-
*
167-
* When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins",
174+
*
175+
* When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins",
168176
* the resulting HTML will appear as (note the 'active' class):
169177
* <pre>
170178
* <ul>
@@ -173,10 +181,10 @@ function $StateRefDirective($state, $timeout) {
173181
* </li>
174182
* </ul>
175183
* </pre>
176-
*
177-
* The class name is interpolated **once** during the directives link time (any further changes to the
178-
* interpolated value are ignored).
179-
*
184+
*
185+
* The class name is interpolated **once** during the directives link time (any further changes to the
186+
* interpolated value are ignored).
187+
*
180188
* Multiple classes may be specified in a space-separated format:
181189
* <pre>
182190
* <ul>
@@ -186,18 +194,37 @@ function $StateRefDirective($state, $timeout) {
186194
* </ul>
187195
* </pre>
188196
*/
189-
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
190-
function $StateActiveDirective($state, $stateParams, $interpolate) {
191-
return {
197+
198+
/**
199+
* @ngdoc directive
200+
* @name ui.router.state.directive:ui-sref-active-equals
201+
*
202+
* @requires ui.router.state.$state
203+
* @requires ui.router.state.$stateParams
204+
* @requires $interpolate
205+
*
206+
* @restrict A
207+
*
208+
* @description
209+
* The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will also activate
210+
* when any descendant state of the ui-sref's target is active. It uses `$state.includes(uiSrefState)`
211+
* to check for activation.
212+
*
213+
*/
214+
$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
215+
function $StateRefActiveDirective($state, $stateParams, $interpolate) {
216+
return {
192217
restrict: "A",
193-
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
218+
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
194219
var state, params, activeClass;
195220

196221
// There probably isn't much point in $observing this
197-
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
222+
// uiSrefActive and uiSrefActiveEquals share the same directive object with some
223+
// slight difference in logic routing
224+
activeClass = $interpolate($attrs.uiSrefActiveEquals || $attrs.uiSrefActive || '', false)($scope);
198225

199-
// Allow uiSref to communicate with uiSrefActive
200-
this.$$setStateInfo = function(newState, newParams) {
226+
// Allow uiSref to communicate with uiSrefActive[Equals]
227+
this.$$setStateInfo = function (newState, newParams) {
201228
state = $state.get(newState, stateContext($element));
202229
params = newParams;
203230
update();
@@ -207,13 +234,21 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {
207234

208235
// Update route state
209236
function update() {
210-
if ($state.$current.self === state && matchesParams()) {
237+
if (isMatch()) {
211238
$element.addClass(activeClass);
212239
} else {
213240
$element.removeClass(activeClass);
214241
}
215242
}
216243

244+
function isMatch() {
245+
if (typeof $attrs.uiSrefActiveEquals !== 'undefined') {
246+
return $state.$current.self === state && matchesParams();
247+
} else {
248+
return $state.includes(state.name) && matchesParams();
249+
}
250+
}
251+
217252
function matchesParams() {
218253
return !params || equalForKeys(params, $stateParams);
219254
}
@@ -223,4 +258,5 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {
223258

224259
angular.module('ui.router.state')
225260
.directive('uiSref', $StateRefDirective)
226-
.directive('uiSrefActive', $StateActiveDirective);
261+
.directive('uiSrefActive', $StateRefActiveDirective)
262+
.directive('uiSrefActiveEquals', $StateRefActiveDirective);

test/stateDirectivesSpec.js

+33-3
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ describe('uiSrefActive', function() {
304304
url: '/:id',
305305
}).state('contacts.item.detail', {
306306
url: '/detail/:foo'
307+
}).state('contacts.item.edit', {
308+
url: '/edit'
307309
});
308310
}));
309311

@@ -312,17 +314,17 @@ describe('uiSrefActive', function() {
312314
}));
313315

314316
it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) {
315-
el = angular.element('<div><a ui-sref="contacts" ui-sref-active="active">Contacts</a></div>');
317+
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>');
316318
template = $compile(el)($rootScope);
317319
$rootScope.$digest();
318320

319321
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
320-
$state.transitionTo('contacts');
322+
$state.transitionTo('contacts.item', { id: 1 });
321323
$q.flush();
322324

323325
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
324326

325-
$state.transitionTo('contacts.item', { id: 5 });
327+
$state.transitionTo('contacts.item', { id: 2 });
326328
$q.flush();
327329
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
328330
}));
@@ -342,6 +344,34 @@ describe('uiSrefActive', function() {
342344
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
343345
}));
344346

347+
it('should match on child states', inject(function($rootScope, $q, $compile, $state) {
348+
template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></div>')($rootScope);
349+
$rootScope.$digest();
350+
var a = angular.element(template[0].getElementsByTagName('a')[0]);
351+
352+
$state.transitionTo('contacts.item.edit', { id: 1 });
353+
$q.flush();
354+
expect(a.attr('class')).toMatch(/active/);
355+
356+
$state.transitionTo('contacts.item.edit', { id: 4 });
357+
$q.flush();
358+
expect(a.attr('class')).not.toMatch(/active/);
359+
}));
360+
361+
it('should NOT match on child states when active-equals is used', inject(function($rootScope, $q, $compile, $state) {
362+
template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active-equals="active">Contacts</a></div>')($rootScope);
363+
$rootScope.$digest();
364+
var a = angular.element(template[0].getElementsByTagName('a')[0]);
365+
366+
$state.transitionTo('contacts.item', { id: 1 });
367+
$q.flush();
368+
expect(a.attr('class')).toMatch(/active/);
369+
370+
$state.transitionTo('contacts.item.edit', { id: 1 });
371+
$q.flush();
372+
expect(a.attr('class')).not.toMatch(/active/);
373+
}));
374+
345375
it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
346376
el = angular.element('<section><div ui-view></div></section>');
347377
template = $compile(el)($rootScope);

0 commit comments

Comments
 (0)