Skip to content

Commit a9ff6fe

Browse files
committed
feat(uiSrefActive): provide a ng-{class,style} like interface
resolves #1431
1 parent db79d76 commit a9ff6fe

File tree

3 files changed

+123
-11
lines changed

3 files changed

+123
-11
lines changed

src/common.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ var isDefined = angular.isDefined,
99
isArray = angular.isArray,
1010
forEach = angular.forEach,
1111
extend = angular.extend,
12-
copy = angular.copy;
12+
copy = angular.copy,
13+
toJson = angular.toJson;
1314

1415
function inherit(parent, extra) {
1516
return extend(new (extend(function() {}, { prototype: parent }))(), extra);

src/stateDirectives.js

+68-10
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,24 @@ function $StateRefDynamicDirective($state, $timeout) {
249249
* </li>
250250
* </ul>
251251
* </pre>
252+
*
253+
* It is also possible to pass ui-sref-active an expression that evaluates
254+
* to an object hash, whose keys represent active class names and whose
255+
* values represent the respective state names/globs.
256+
* ui-sref-active will match if the current active state **includes** any of
257+
* the specified state names/globs, even the abstract ones.
258+
*
259+
* @Example
260+
* Given the following template, with "admin" being an abstract state:
261+
* <pre>
262+
* <div ui-sref-active="{'active': 'admin.*'}">
263+
* <a ui-sref-active="active" ui-sref="admin.roles">Roles</a>
264+
* </div>
265+
* </pre>
266+
*
267+
* When the current state is "admin.roles" the "active" class will be applied
268+
* to both the <div> and <a> elements. It is important to note that the state
269+
* names/globs passed to ui-sref-active shadow the state provided by ui-sref.
252270
*/
253271

254272
/**
@@ -271,35 +289,75 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
271289
return {
272290
restrict: "A",
273291
controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) {
274-
var states = [], activeClass, activeEqClass;
292+
var states = [], activeClasses = {}, activeEqClass;
275293

276294
// There probably isn't much point in $observing this
277295
// uiSrefActive and uiSrefActiveEq share the same directive object with some
278296
// slight difference in logic routing
279-
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
280297
activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope);
281298

299+
var uiSrefActive = $scope.$eval($attrs.uiSrefActive) || $interpolate($attrs.uiSrefActive || '', false)($scope);
300+
if (isObject(uiSrefActive)) {
301+
forEach(uiSrefActive, function(stateOrName, activeClass) {
302+
if (isString(stateOrName)) {
303+
var ref = parseStateRef(stateOrName, $state.current.name);
304+
addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
305+
}
306+
});
307+
}
308+
282309
// Allow uiSref to communicate with uiSrefActive[Equals]
283310
this.$$addStateInfo = function (newState, newParams) {
284-
var state = $state.get(newState, stateContext($element));
311+
// we already got an explicit state provided by ui-sref-active, so we
312+
// shadow the one that comes from ui-sref
313+
if (isObject(uiSrefActive) && states.length > 0) {
314+
return;
315+
}
316+
addState(newState, newParams, uiSrefActive);
317+
update();
318+
};
319+
320+
$scope.$on('$stateChangeSuccess', update);
321+
322+
function addState(stateName, stateParams, activeClass) {
323+
var state = $state.get(stateName, stateContext($element));
324+
var stateHash = createStateHash(stateName, stateParams);
285325

286326
states.push({
287-
state: state || { name: newState },
288-
params: newParams
327+
state: state || { name: stateName },
328+
params: stateParams,
329+
hash: stateHash
289330
});
290331

291-
update();
292-
};
332+
activeClasses[stateHash] = activeClass;
333+
}
293334

294-
$scope.$on('$stateChangeSuccess', update);
335+
/**
336+
* @param {string} state
337+
* @param {Object|string} [params]
338+
* @return {string}
339+
*/
340+
function createStateHash(state, params) {
341+
if (!isString(state)) {
342+
throw new Error('state should be a string');
343+
}
344+
if (isObject(params)) {
345+
return state + toJson(params);
346+
}
347+
params = $scope.$eval(params);
348+
if (isObject(params)) {
349+
return state + toJson(params);
350+
}
351+
return state;
352+
}
295353

296354
// Update route state
297355
function update() {
298356
for (var i = 0; i < states.length; i++) {
299357
if (anyMatch(states[i].state, states[i].params)) {
300-
addClass($element, activeClass);
358+
addClass($element, activeClasses[states[i].hash]);
301359
} else {
302-
removeClass($element, activeClass);
360+
removeClass($element, activeClasses[states[i].hash]);
303361
}
304362

305363
if (exactMatch(states[i].state, states[i].params)) {

test/stateDirectivesSpec.js

+53
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@ describe('uiSrefActive', function() {
451451
url: '/detail/:foo'
452452
}).state('contacts.item.edit', {
453453
url: '/edit'
454+
}).state('admin', {
455+
url: '/admin',
456+
abstract: true,
457+
template: '<ui-view/>'
458+
}).state('admin.roles', {
459+
url: '/roles?page'
454460
});
455461
}));
456462

@@ -628,6 +634,53 @@ describe('uiSrefActive', function() {
628634
timeoutFlush();
629635
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
630636
}));
637+
638+
describe('ng-{class,style} interface', function() {
639+
it('should match on abstract states that are included by the current state', inject(function($rootScope, $compile, $state, $q) {
640+
el = $compile('<div ui-sref-active="{active: \'admin.*\'}"><a ui-sref-active="active" ui-sref="admin.roles">Roles</a></div>')($rootScope);
641+
$state.transitionTo('admin.roles');
642+
$q.flush();
643+
timeoutFlush();
644+
var abstractParent = el[0];
645+
expect(abstractParent.className).toMatch(/active/);
646+
var child = el[0].querySelector('a');
647+
expect(child.className).toMatch(/active/);
648+
}));
649+
650+
it('should match on state parameters', inject(function($compile, $rootScope, $state, $q) {
651+
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"></div>')($rootScope);
652+
$state.transitionTo('admin.roles', {page: 1});
653+
$q.flush();
654+
timeoutFlush();
655+
expect(el[0].className).toMatch(/active/);
656+
}));
657+
658+
it('should shadow the state provided by ui-sref', inject(function($compile, $rootScope, $state, $q) {
659+
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"><a ui-sref="admin.roles"></a></div>')($rootScope);
660+
$state.transitionTo('admin.roles');
661+
$q.flush();
662+
timeoutFlush();
663+
expect(el[0].className).not.toMatch(/active/);
664+
$state.transitionTo('admin.roles', {page: 1});
665+
$q.flush();
666+
timeoutFlush();
667+
expect(el[0].className).toMatch(/active/);
668+
}));
669+
670+
it('should support multiple <className, stateOrName> pairs', inject(function($compile, $rootScope, $state, $q) {
671+
el = $compile('<div ui-sref-active="{contacts: \'contacts.*\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
672+
$state.transitionTo('contacts');
673+
$q.flush();
674+
timeoutFlush();
675+
expect(el[0].className).toMatch(/contacts/);
676+
expect(el[0].className).not.toMatch(/admin/);
677+
$state.transitionTo('admin.roles', {page: 1});
678+
$q.flush();
679+
timeoutFlush();
680+
expect(el[0].className).toMatch(/admin/);
681+
expect(el[0].className).not.toMatch(/contacts/);
682+
}));
683+
});
631684
});
632685

633686
describe('uiView controllers or onEnter handlers', function() {

0 commit comments

Comments
 (0)