Skip to content

Commit dde5d68

Browse files
nateabeleCaitlin Potter
authored andcommitted
chore(uiStateActive): refactor & add test coverage
1 parent 3462ccb commit dde5d68

File tree

4 files changed

+167
-67
lines changed

4 files changed

+167
-67
lines changed

src/common.js

Lines changed: 73 additions & 0 deletions
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

Lines changed: 2 additions & 35 deletions
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

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +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',
1119
require: '?^uiStateActive',
1220
link: function(scope, element, attrs, uiStateActive) {
1321
var ref = parseStateRef(attrs.uiSref);
14-
var params = null, url = null, base = $state.$current;
22+
var params = null, url = null, base = stateContext(element) || $state.$current;
1523
var isForm = element[0].nodeName === "FORM";
1624
var attr = isForm ? "action" : "href", nav = true;
1725

18-
var stateData = element.parent().inheritedData('$uiView');
19-
20-
if (stateData && stateData.state && stateData.state.name) {
21-
base = stateData.state;
22-
}
23-
2426
var update = function(newVal) {
2527
if (newVal) params = newVal;
2628
if (!nav) return;
@@ -69,47 +71,29 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {
6971
var state, params, paramKeys, activeClass;
7072

7173
// There probably isn't much point in $observing this
72-
activeClass = $interpolate($attrs.uiSactive || '', false)($scope);
74+
activeClass = $interpolate($attrs.uiStateActive || '', false)($scope);
7375

7476
// Allow uiSref to communicate with uiStateActive
7577
this.$$setStateInfo = function(newState, newParams) {
76-
state = newState;
78+
state = $state.get(newState, stateContext($element));
7779
params = newParams;
78-
paramKeys = params && Object.keys(params);
80+
paramKeys = params && keys(params);
7981
update();
8082
};
8183

8284
$scope.$on('$stateChangeSuccess', update);
83-
$scope.$on('$stateChangeError', function() {
84-
$attrs.$removeClass(activeClass);
85-
});
8685

8786
// Update route state
8887
function update() {
89-
if ($state.current.name === state && matchesParams()) {
90-
$attrs.$addClass(activeClass);
88+
if ($state.$current.self === state && matchesParams()) {
89+
$element.addClass(activeClass);
9190
} else {
92-
$attrs.$removeClass(activeClass);
91+
$element.removeClass(activeClass);
9392
}
9493
}
9594

9695
function matchesParams() {
97-
if (params) {
98-
var result = true;
99-
// Can't use angular.equals() because it is possible for ui-sref
100-
// to not reference each state parameter in $stateParams
101-
//
102-
// Unfortunately, using angular.forEach, short-circuiting is
103-
// impossible --- But it's unlikely that very many parameters are
104-
// used, so it is unlikely to hurt badly.
105-
angular.forEach(params, function(value, key) {
106-
if ($stateParams[key] !== value) {
107-
result = false;
108-
}
109-
});
110-
return result;
111-
}
112-
return true;
96+
return !params || equalForKeys(params, $stateParams);
11397
}
11498
}
11599
};

test/stateDirectivesSpec.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,79 @@ describe('uiStateRef', function() {
217217
}));
218218
});
219219
});
220+
221+
describe('uiStateActive', 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-state-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-state-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-state-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)