Skip to content

Commit ae360fc

Browse files
committed
Merge pull request #635 from caitp/issue-19
feat(uiStateActive): directive to add/remove classes for active state
2 parents 0323fa4 + 4cea4ed commit ae360fc

File tree

4 files changed

+203
-44
lines changed

4 files changed

+203
-44
lines changed

src/common.js

+73
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ function ancestors(first, second) {
4444
return path;
4545
}
4646

47+
/**
48+
* IE8-safe wrapper for `Object.keys()`.
49+
*
50+
* @param {Object} object A JavaScript object.
51+
* @return {Array} Returns the keys of the object as an array.
52+
*/
53+
function keys(object) {
54+
if (Object.keys) {
55+
return Object.keys(object);
56+
}
57+
var result = [];
58+
59+
angular.forEach(object, function(val, key) {
60+
result.push(key);
61+
});
62+
return result;
63+
}
64+
4765
/**
4866
* IE8-safe wrapper for `Array.prototype.indexOf()`.
4967
*
@@ -91,6 +109,61 @@ function inheritParams(currentParams, newParams, $current, $to) {
91109
return extend({}, inherited, newParams);
92110
}
93111

112+
/**
113+
* Normalizes a set of values to string or `null`, filtering them by a list of keys.
114+
*
115+
* @param {Array} keys The list of keys to normalize/return.
116+
* @param {Object} values An object hash of values to normalize.
117+
* @return {Object} Returns an object hash of normalized string values.
118+
*/
119+
function normalize(keys, values) {
120+
var normalized = {};
121+
122+
forEach(keys, function (name) {
123+
var value = values[name];
124+
normalized[name] = (value != null) ? String(value) : null;
125+
});
126+
return normalized;
127+
}
128+
129+
/**
130+
* Performs a non-strict comparison of the subset of two objects, defined by a list of keys.
131+
*
132+
* @param {Object} a The first object.
133+
* @param {Object} b The second object.
134+
* @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified,
135+
* it defaults to the list of keys in `a`.
136+
* @return {Boolean} Returns `true` if the keys match, otherwise `false`.
137+
*/
138+
function equalForKeys(a, b, keys) {
139+
if (!keys) {
140+
keys = [];
141+
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
142+
}
143+
144+
for (var i=0; i<keys.length; i++) {
145+
var k = keys[i];
146+
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
147+
}
148+
return true;
149+
}
150+
151+
/**
152+
* Returns the subset of an object, based on a list of keys.
153+
*
154+
* @param {Array} keys
155+
* @param {Object} values
156+
* @return {Boolean} Returns a subset of `values`.
157+
*/
158+
function filterByKeys(keys, values) {
159+
var filtered = {};
160+
161+
forEach(keys, function (name) {
162+
filtered[name] = values[name];
163+
});
164+
return filtered;
165+
}
166+
94167
angular.module('ui.router.util', ['ng']);
95168
angular.module('ui.router.router', ['ui.router.util']);
96169
angular.module('ui.router.state', ['ui.router.router', 'ui.router.util']);

src/state.js

+2-35
Original file line numberDiff line numberDiff line change
@@ -488,13 +488,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
488488
return url;
489489
};
490490

491-
$state.get = function (stateOrName) {
491+
$state.get = function (stateOrName, context) {
492492
if (!isDefined(stateOrName)) {
493493
var list = [];
494494
forEach(states, function(state) { list.push(state.self); });
495495
return list;
496496
}
497-
var state = findState(stateOrName);
497+
var state = findState(stateOrName, context);
498498
return (state && state.self) ? state.self : null;
499499
};
500500

@@ -546,39 +546,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
546546
return $state;
547547
}
548548

549-
function normalize(keys, values) {
550-
var normalized = {};
551-
552-
forEach(keys, function (name) {
553-
var value = values[name];
554-
normalized[name] = (value != null) ? String(value) : null;
555-
});
556-
return normalized;
557-
}
558-
559-
function equalForKeys(a, b, keys) {
560-
// If keys not provided, assume keys from object 'a'
561-
if (!keys) {
562-
keys = [];
563-
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
564-
}
565-
566-
for (var i=0; i<keys.length; i++) {
567-
var k = keys[i];
568-
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
569-
}
570-
return true;
571-
}
572-
573-
function filterByKeys(keys, values) {
574-
var filtered = {};
575-
576-
forEach(keys, function (name) {
577-
filtered[name] = values[name];
578-
});
579-
return filtered;
580-
}
581-
582549
function shouldTriggerReload(to, from, locals, options) {
583550
if ( to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false)) ) {
584551
return true;

src/stateDirectives.js

+52-9
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,25 @@ function parseStateRef(ref) {
44
return { state: parsed[1], paramExpr: parsed[3] || null };
55
}
66

7+
function stateContext(el) {
8+
var stateData = el.parent().inheritedData('$uiView');
9+
10+
if (stateData && stateData.state && stateData.state.name) {
11+
return stateData.state;
12+
}
13+
}
14+
715
$StateRefDirective.$inject = ['$state'];
816
function $StateRefDirective($state) {
917
return {
1018
restrict: 'A',
11-
link: function(scope, element, attrs) {
19+
require: '?^uiSrefActive',
20+
link: function(scope, element, attrs, uiSrefActive) {
1221
var ref = parseStateRef(attrs.uiSref);
13-
var params = null, url = null, base = $state.$current;
22+
var params = null, url = null, base = stateContext(element) || $state.$current;
1423
var isForm = element[0].nodeName === "FORM";
1524
var attr = isForm ? "action" : "href", nav = true;
1625

17-
var stateData = element.parent().inheritedData('$uiView');
18-
19-
if (stateData && stateData.state && stateData.state.name) {
20-
base = stateData.state;
21-
}
22-
2326
var update = function(newVal) {
2427
if (newVal) params = newVal;
2528
if (!nav) return;
@@ -31,6 +34,9 @@ function $StateRefDirective($state) {
3134
return false;
3235
}
3336
element[0][attr] = newHref;
37+
if (uiSrefActive) {
38+
uiSrefActive.$$setStateInfo(ref.state, params);
39+
}
3440
};
3541

3642
if (ref.paramExpr) {
@@ -57,4 +63,41 @@ function $StateRefDirective($state) {
5763
};
5864
}
5965

60-
angular.module('ui.router.state').directive('uiSref', $StateRefDirective);
66+
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
67+
function $StateActiveDirective($state, $stateParams, $interpolate) {
68+
return {
69+
restrict: "A",
70+
controller: function($scope, $element, $attrs) {
71+
var state, params, activeClass;
72+
73+
// There probably isn't much point in $observing this
74+
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
75+
76+
// Allow uiSref to communicate with uiSrefActive
77+
this.$$setStateInfo = function(newState, newParams) {
78+
state = $state.get(newState, stateContext($element));
79+
params = newParams;
80+
update();
81+
};
82+
83+
$scope.$on('$stateChangeSuccess', update);
84+
85+
// Update route state
86+
function update() {
87+
if ($state.$current.self === state && matchesParams()) {
88+
$element.addClass(activeClass);
89+
} else {
90+
$element.removeClass(activeClass);
91+
}
92+
}
93+
94+
function matchesParams() {
95+
return !params || equalForKeys(params, $stateParams);
96+
}
97+
}
98+
};
99+
}
100+
101+
angular.module('ui.router.state')
102+
.directive('uiSref', $StateRefDirective)
103+
.directive('uiSrefActive', $StateActiveDirective);

test/stateDirectivesSpec.js

+76
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,79 @@ describe('uiStateRef', function() {
217217
}));
218218
});
219219
});
220+
221+
describe('uiSrefActive', function() {
222+
var el, template, scope, document;
223+
224+
beforeEach(module('ui.router'));
225+
226+
beforeEach(module(function($stateProvider) {
227+
$stateProvider.state('index', {
228+
url: '',
229+
}).state('contacts', {
230+
url: '/contacts',
231+
views: {
232+
'@': {
233+
template: '<a ui-sref=".item({ id: 6 })" ui-sref-active="active">Contacts</a>'
234+
}
235+
}
236+
}).state('contacts.item', {
237+
url: '/:id',
238+
}).state('contacts.item.detail', {
239+
url: '/detail/:foo'
240+
});
241+
}));
242+
243+
beforeEach(inject(function($document) {
244+
document = $document[0];
245+
}));
246+
247+
it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) {
248+
el = angular.element('<div><a ui-sref="contacts" ui-sref-active="active">Contacts</a></div>');
249+
template = $compile(el)($rootScope);
250+
$rootScope.$digest();
251+
252+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
253+
$state.transitionTo('contacts');
254+
$q.flush();
255+
256+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
257+
258+
$state.transitionTo('contacts.item', { id: 5 });
259+
$q.flush();
260+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
261+
}));
262+
263+
it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) {
264+
el = angular.element('<div><a ui-sref="contacts.item.detail({ foo: \'bar\' })" ui-sref-active="active">Contacts</a></div>');
265+
template = $compile(el)($rootScope);
266+
$rootScope.$digest();
267+
268+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
269+
$state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' });
270+
$q.flush();
271+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
272+
273+
$state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' });
274+
$q.flush();
275+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
276+
}));
277+
278+
it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
279+
el = angular.element('<section><div ui-view></div></section>');
280+
template = $compile(el)($rootScope);
281+
$rootScope.$digest();
282+
283+
$state.transitionTo('contacts');
284+
$q.flush();
285+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
286+
287+
$state.transitionTo('contacts.item', { id: 6 });
288+
$q.flush();
289+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');
290+
291+
$state.transitionTo('contacts.item', { id: 5 });
292+
$q.flush();
293+
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
294+
}));
295+
});

0 commit comments

Comments
 (0)