Skip to content

Commit bf163ad

Browse files
committed
feat(uiSrefActive): Also activate for child states.
To limit activation to target state use new `ui-sref-active-eq` 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-eq, thought typically we think devs want the auto inheritance. Fixes #818
1 parent e3ba1bf commit bf163ad

File tree

2 files changed

+90
-23
lines changed

2 files changed

+90
-23
lines changed

src/stateDirectives.js

+57-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', '?^uiSrefActiveEq'],
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,20 @@ 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+
* ui-sref-active found at the same level or above the ui-sref will be used.
160+
*
161+
* Will activate when the ui-sref's target state or any child state is active. If you
162+
* need to activate only when the ui-sref target state is active and *not* any of
163+
* it's children, then you will use
164+
* {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq}
165+
*
157166
* @example
158167
* Given the following template:
159168
* <pre>
@@ -163,8 +172,9 @@ function $StateRefDirective($state, $timeout) {
163172
* </li>
164173
* </ul>
165174
* </pre>
166-
*
167-
* When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins",
175+
*
176+
*
177+
* When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins",
168178
* the resulting HTML will appear as (note the 'active' class):
169179
* <pre>
170180
* <ul>
@@ -173,10 +183,10 @@ function $StateRefDirective($state, $timeout) {
173183
* </li>
174184
* </ul>
175185
* </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-
*
186+
*
187+
* The class name is interpolated **once** during the directives link time (any further changes to the
188+
* interpolated value are ignored).
189+
*
180190
* Multiple classes may be specified in a space-separated format:
181191
* <pre>
182192
* <ul>
@@ -186,18 +196,36 @@ function $StateRefDirective($state, $timeout) {
186196
* </ul>
187197
* </pre>
188198
*/
189-
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
190-
function $StateActiveDirective($state, $stateParams, $interpolate) {
191-
return {
199+
200+
/**
201+
* @ngdoc directive
202+
* @name ui.router.state.directive:ui-sref-active-eq
203+
*
204+
* @requires ui.router.state.$state
205+
* @requires ui.router.state.$stateParams
206+
* @requires $interpolate
207+
*
208+
* @restrict A
209+
*
210+
* @description
211+
* The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will will only activate
212+
* when the exact target state used in the `ui-sref` is active; no child states.
213+
*
214+
*/
215+
$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
216+
function $StateRefActiveDirective($state, $stateParams, $interpolate) {
217+
return {
192218
restrict: "A",
193-
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
219+
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
194220
var state, params, activeClass;
195221

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

199-
// Allow uiSref to communicate with uiSrefActive
200-
this.$$setStateInfo = function(newState, newParams) {
227+
// Allow uiSref to communicate with uiSrefActive[Equals]
228+
this.$$setStateInfo = function (newState, newParams) {
201229
state = $state.get(newState, stateContext($element));
202230
params = newParams;
203231
update();
@@ -207,13 +235,21 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {
207235

208236
// Update route state
209237
function update() {
210-
if ($state.$current.self === state && matchesParams()) {
238+
if (isMatch()) {
211239
$element.addClass(activeClass);
212240
} else {
213241
$element.removeClass(activeClass);
214242
}
215243
}
216244

245+
function isMatch() {
246+
if (typeof $attrs.uiSrefActiveEq !== 'undefined') {
247+
return $state.$current.self === state && matchesParams();
248+
} else {
249+
return $state.includes(state.name) && matchesParams();
250+
}
251+
}
252+
217253
function matchesParams() {
218254
return !params || equalForKeys(params, $stateParams);
219255
}
@@ -223,4 +259,5 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {
223259

224260
angular.module('ui.router.state')
225261
.directive('uiSref', $StateRefDirective)
226-
.directive('uiSrefActive', $StateActiveDirective);
262+
.directive('uiSrefActive', $StateRefActiveDirective)
263+
.directive('uiSrefActiveEq', $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-eq="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)