Skip to content

Commit 65bfb7b

Browse files
committed
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 angular-ui#704.
1 parent cf34271 commit 65bfb7b

File tree

2 files changed

+134
-42
lines changed

2 files changed

+134
-42
lines changed

src/stateDirectives.js

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function stateContext(el) {
1717
* @name ui.router.state.directive:ui-sref
1818
*
1919
* @requires ui.router.state.$state
20+
* @requires ui.router.state.$stateParams
2021
* @requires $timeout
2122
*
2223
* @restrict A
@@ -48,14 +49,13 @@ function stateContext(el) {
4849
*
4950
* @param {string} ui-sref 'stateName' can be any valid absolute or relative state
5051
*/
51-
$StateRefDirective.$inject = ['$state', '$timeout'];
52-
function $StateRefDirective($state, $timeout) {
52+
$StateRefDirective.$inject = ['$state', '$timeout', '$stateParams'];
53+
function $StateRefDirective($state, $timeout, $stateParams) {
5354
return {
5455
restrict: 'A',
55-
require: '?^uiSrefActive',
56-
link: function(scope, element, attrs, uiSrefActive) {
56+
link: function(scope, element, attrs) {
5757
var ref = parseStateRef(attrs.uiSref);
58-
var params = null, url = null, base = stateContext(element) || $state.$current;
58+
var state = null, params = null, url = null, base = stateContext(element) || $state.$current;
5959
var isForm = element[0].nodeName === "FORM";
6060
var attr = isForm ? "action" : "href", nav = true;
6161

@@ -64,10 +64,8 @@ function $StateRefDirective($state, $timeout) {
6464
if (!nav) return;
6565

6666
var newHref = $state.href(ref.state, params, { relative: base });
67+
state = $state.get(ref.state, base);
6768

68-
if (uiSrefActive) {
69-
uiSrefActive.$$setStateInfo(ref.state, params);
70-
}
7169
if (!newHref) {
7270
nav = false;
7371
return false;
@@ -95,6 +93,37 @@ function $StateRefDirective($state, $timeout) {
9593
e.preventDefault();
9694
}
9795
});
96+
97+
var emitEvents = function(){
98+
// HACK:
99+
// Emits events only after
100+
// 1. The execution of link functions of ancestor's ui-sref-active
101+
// or,
102+
// 2. The ancestor ui-sref-active has removed their previously appended classes.
103+
//
104+
$timeout(function(){
105+
if($state.$current.self === state && matchesParams()){
106+
// Exact match of current state
107+
scope.$emit('$uiSrefActivated');
108+
}else if($state.includes(state.name) && matchesParams()){
109+
// The current state is a child of reference state
110+
scope.$emit('$uiSrefChildStateActivated');
111+
}
112+
});
113+
};
114+
115+
// Emits $uiSref*Activated events.
116+
scope.$on('$stateChangeSuccess', emitEvents);
117+
118+
// Also emits the events when the element is first created (linked).
119+
// This makes sure the events are emitted if a state is directly navigated
120+
// through the browser navigation bar.
121+
//
122+
emitEvents();
123+
124+
function matchesParams() {
125+
return !params || equalForKeys(params, $stateParams);
126+
}
98127
}
99128
};
100129
}
@@ -104,7 +133,6 @@ function $StateRefDirective($state, $timeout) {
104133
* @name ui.router.state.directive:ui-sref-active
105134
*
106135
* @requires ui.router.state.$state
107-
* @requires ui.router.state.$stateParams
108136
* @requires $interpolate
109137
*
110138
* @restrict A
@@ -126,38 +154,33 @@ function $StateRefDirective($state, $timeout) {
126154
* </ul>
127155
* </pre>
128156
*/
129-
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
130-
function $StateActiveDirective($state, $stateParams, $interpolate) {
157+
$StateActiveDirective.$inject = ['$state', '$interpolate'];
158+
function $StateActiveDirective($state, $interpolate) {
131159
return {
132160
restrict: "A",
133-
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
134-
var state, params, activeClass;
161+
scope: true, // Catching $uiSref*Activated events without sibling's interferance.
162+
link: function(scope, element, attrs) {
163+
var activeClass, activeClassNested, activeClassList;
135164

136165
// There probably isn't much point in $observing this
137-
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
166+
activeClass = $interpolate(attrs.uiSrefActive || '', false)(scope);
167+
activeClassNested = activeClass + '-nested';
168+
activeClassList = [activeClass, activeClassNested].join(' '); // space-separated list of all appended classes
138169

139-
// Allow uiSref to communicate with uiSrefActive
140-
this.$$setStateInfo = function(newState, newParams) {
141-
state = $state.get(newState, stateContext($element));
142-
params = newParams;
143-
update();
144-
};
170+
// Remove all previously appended classes.
171+
scope.$on('$stateChangeSuccess', function(){
172+
element.removeClass(activeClassList);
173+
});
145174

146-
$scope.$on('$stateChangeSuccess', update);
175+
scope.$on('$uiSrefActivated', function(){
176+
element.addClass(activeClass);
177+
});
147178

148-
// Update route state
149-
function update() {
150-
if ($state.$current.self === state && matchesParams()) {
151-
$element.addClass(activeClass);
152-
} else {
153-
$element.removeClass(activeClass);
154-
}
155-
}
179+
scope.$on('$uiSrefChildStateActivated', function(){
180+
element.addClass(activeClassNested);
181+
});
156182

157-
function matchesParams() {
158-
return !params || equalForKeys(params, $stateParams);
159-
}
160-
}]
183+
}
161184
};
162185
}
163186

test/stateDirectivesSpec.js

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,58 +278,127 @@ describe('uiSrefActive', function() {
278278
url: '/:id',
279279
}).state('contacts.item.detail', {
280280
url: '/detail/:foo'
281+
}).state('contacts.item.edit', {
282+
url: '/edit'
281283
});
282284
}));
283285

284286
beforeEach(inject(function($document) {
285287
document = $document[0];
286288
}));
287289

288-
it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) {
290+
it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state, $timeout) {
289291
el = angular.element('<div><a ui-sref="contacts" ui-sref-active="active">Contacts</a></div>');
290292
template = $compile(el)($rootScope);
291293
$rootScope.$digest();
294+
$timeout.flush(); // emitEvent timeout hacks
292295

293-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
296+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
297+
$state.transitionTo('contacts');
298+
$timeout.flush(); // emitEvent timeout hacks
299+
$q.flush();
300+
301+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');
302+
303+
$state.transitionTo('contacts.item', { id: 5 });
304+
$timeout.flush(); // emitEvent timeout hacks
305+
$q.flush();
306+
307+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active-nested');
308+
}));
309+
310+
it('should update class for decendant uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) {
311+
el = angular.element('<section><div ui-sref-active="active"><p ng-init="newScope=1"><a ui-sref="contacts">Contacts</a></p></div></section>');
312+
template = $compile(el)($rootScope);
313+
$rootScope.$digest();
314+
$timeout.flush(); // emitEvent timeout hacks
315+
316+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');
294317
$state.transitionTo('contacts');
318+
$timeout.flush(); // emitEvent timeout hacks
295319
$q.flush();
296320

297-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
321+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope active');
298322

299323
$state.transitionTo('contacts.item', { id: 5 });
324+
$timeout.flush(); // emitEvent timeout hacks
300325
$q.flush();
301-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
326+
327+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope active-nested');
302328
}));
303329

304-
it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) {
330+
it('should not update class for sibling elements with uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) {
331+
el = angular.element('<section><div ui-sref-active="active"></div><a ui-sref="contacts">Contacts</a></section>');
332+
template = $compile(el)($rootScope);
333+
$rootScope.$digest();
334+
$timeout.flush(); // emitEvent timeout hacks
335+
336+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');
337+
$state.transitionTo('contacts');
338+
$timeout.flush(); // emitEvent timeout hacks
339+
$q.flush();
340+
341+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');
342+
343+
$state.transitionTo('contacts.item', { id: 5 });
344+
$timeout.flush(); // emitEvent timeout hacks
345+
$q.flush();
346+
347+
expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');
348+
}));
349+
350+
it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state, $timeout) {
305351
el = angular.element('<div><a ui-sref="contacts.item.detail({ foo: \'bar\' })" ui-sref-active="active">Contacts</a></div>');
306352
template = $compile(el)($rootScope);
307353
$rootScope.$digest();
354+
$timeout.flush(); // emitEvent timeout hacks
308355

309-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
356+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
310357
$state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' });
311358
$q.flush();
312-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
359+
$timeout.flush(); // emitEvent timeout hacks
360+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');
313361

314362
$state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' });
315363
$q.flush();
316-
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
364+
$timeout.flush(); // emitEvent timeout hacks
365+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
366+
}));
367+
368+
it('should match child states', inject(function($rootScope, $q, $compile, $state, $timeout) {
369+
el = angular.element('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></div>');
370+
template = $compile(el)($rootScope);
371+
$rootScope.$digest();
372+
$timeout.flush(); // emitEvent timeout hacks
373+
374+
$state.transitionTo('contacts.item.edit', { id: 1 });
375+
$q.flush();
376+
$timeout.flush(); // emitEvent timeout hacks
377+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active-nested');
378+
379+
$state.transitionTo('contacts.item.edit', { id: 4 });
380+
$q.flush();
381+
$timeout.flush(); // emitEvent timeout hacks
382+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
317383
}));
318384

319-
it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
385+
it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state, $timeout) {
320386
el = angular.element('<section><div ui-view></div></section>');
321387
template = $compile(el)($rootScope);
322388
$rootScope.$digest();
323389

324390
$state.transitionTo('contacts');
391+
$timeout.flush(); // emitEvent timeout hacks
325392
$q.flush();
326393
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
327394

328395
$state.transitionTo('contacts.item', { id: 6 });
396+
$timeout.flush(); // emitEvent timeout hacks
329397
$q.flush();
330398
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');
331399

332400
$state.transitionTo('contacts.item', { id: 5 });
401+
$timeout.flush(); // emitEvent timeout hacks
333402
$q.flush();
334403
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
335404
}));

0 commit comments

Comments
 (0)