diff --git a/src/common.js b/src/common.js
index a85ff9e73..fbe00aac0 100644
--- a/src/common.js
+++ b/src/common.js
@@ -9,7 +9,8 @@ var isDefined = angular.isDefined,
isArray = angular.isArray,
forEach = angular.forEach,
extend = angular.extend,
- copy = angular.copy;
+ copy = angular.copy,
+ toJson = angular.toJson;
function inherit(parent, extra) {
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
diff --git a/src/stateDirectives.js b/src/stateDirectives.js
index c55a82091..d244df5c2 100644
--- a/src/stateDirectives.js
+++ b/src/stateDirectives.js
@@ -249,6 +249,24 @@ function $StateRefDynamicDirective($state, $timeout) {
*
*
*
+ *
+ * It is also possible to pass ui-sref-active an expression that evaluates
+ * to an object hash, whose keys represent active class names and whose
+ * values represent the respective state names/globs.
+ * ui-sref-active will match if the current active state **includes** any of
+ * the specified state names/globs, even the abstract ones.
+ *
+ * @Example
+ * Given the following template, with "admin" being an abstract state:
+ *
+ *
+ *
+ *
+ * When the current state is "admin.roles" the "active" class will be applied
+ * to both the and
elements. It is important to note that the state
+ * names/globs passed to ui-sref-active shadow the state provided by ui-sref.
*/
/**
@@ -271,35 +289,75 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) {
- var states = [], activeClass, activeEqClass;
+ var states = [], activeClasses = {}, activeEqClass;
// There probably isn't much point in $observing this
// uiSrefActive and uiSrefActiveEq share the same directive object with some
// slight difference in logic routing
- activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope);
+ var uiSrefActive = $scope.$eval($attrs.uiSrefActive) || $interpolate($attrs.uiSrefActive || '', false)($scope);
+ if (isObject(uiSrefActive)) {
+ forEach(uiSrefActive, function(stateOrName, activeClass) {
+ if (isString(stateOrName)) {
+ var ref = parseStateRef(stateOrName, $state.current.name);
+ addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
+ }
+ });
+ }
+
// Allow uiSref to communicate with uiSrefActive[Equals]
this.$$addStateInfo = function (newState, newParams) {
- var state = $state.get(newState, stateContext($element));
+ // we already got an explicit state provided by ui-sref-active, so we
+ // shadow the one that comes from ui-sref
+ if (isObject(uiSrefActive) && states.length > 0) {
+ return;
+ }
+ addState(newState, newParams, uiSrefActive);
+ update();
+ };
+
+ $scope.$on('$stateChangeSuccess', update);
+
+ function addState(stateName, stateParams, activeClass) {
+ var state = $state.get(stateName, stateContext($element));
+ var stateHash = createStateHash(stateName, stateParams);
states.push({
- state: state || { name: newState },
- params: newParams
+ state: state || { name: stateName },
+ params: stateParams,
+ hash: stateHash
});
- update();
- };
+ activeClasses[stateHash] = activeClass;
+ }
- $scope.$on('$stateChangeSuccess', update);
+ /**
+ * @param {string} state
+ * @param {Object|string} [params]
+ * @return {string}
+ */
+ function createStateHash(state, params) {
+ if (!isString(state)) {
+ throw new Error('state should be a string');
+ }
+ if (isObject(params)) {
+ return state + toJson(params);
+ }
+ params = $scope.$eval(params);
+ if (isObject(params)) {
+ return state + toJson(params);
+ }
+ return state;
+ }
// Update route state
function update() {
for (var i = 0; i < states.length; i++) {
if (anyMatch(states[i].state, states[i].params)) {
- addClass($element, activeClass);
+ addClass($element, activeClasses[states[i].hash]);
} else {
- removeClass($element, activeClass);
+ removeClass($element, activeClasses[states[i].hash]);
}
if (exactMatch(states[i].state, states[i].params)) {
@@ -309,6 +367,7 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
}
}
}
+ update();
function addClass(el, className) { $timeout(function () { el.addClass(className); }); }
function removeClass(el, className) { el.removeClass(className); }
diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js
index 4985ef542..f0271615d 100644
--- a/test/stateDirectivesSpec.js
+++ b/test/stateDirectivesSpec.js
@@ -451,6 +451,12 @@ describe('uiSrefActive', function() {
url: '/detail/:foo'
}).state('contacts.item.edit', {
url: '/edit'
+ }).state('admin', {
+ url: '/admin',
+ abstract: true,
+ template: ''
+ }).state('admin.roles', {
+ url: '/roles?page'
});
}));
@@ -628,6 +634,53 @@ describe('uiSrefActive', function() {
timeoutFlush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
}));
+
+ describe('ng-{class,style} interface', function() {
+ it('should match on abstract states that are included by the current state', inject(function($rootScope, $compile, $state, $q) {
+ el = $compile('')($rootScope);
+ $state.transitionTo('admin.roles');
+ $q.flush();
+ timeoutFlush();
+ var abstractParent = el[0];
+ expect(abstractParent.className).toMatch(/active/);
+ var child = el[0].querySelector('a');
+ expect(child.className).toMatch(/active/);
+ }));
+
+ it('should match on state parameters', inject(function($compile, $rootScope, $state, $q) {
+ el = $compile('')($rootScope);
+ $state.transitionTo('admin.roles', {page: 1});
+ $q.flush();
+ timeoutFlush();
+ expect(el[0].className).toMatch(/active/);
+ }));
+
+ it('should shadow the state provided by ui-sref', inject(function($compile, $rootScope, $state, $q) {
+ el = $compile('')($rootScope);
+ $state.transitionTo('admin.roles');
+ $q.flush();
+ timeoutFlush();
+ expect(el[0].className).not.toMatch(/active/);
+ $state.transitionTo('admin.roles', {page: 1});
+ $q.flush();
+ timeoutFlush();
+ expect(el[0].className).toMatch(/active/);
+ }));
+
+ it('should support multiple pairs', inject(function($compile, $rootScope, $state, $q) {
+ el = $compile('')($rootScope);
+ $state.transitionTo('contacts');
+ $q.flush();
+ timeoutFlush();
+ expect(el[0].className).toMatch(/contacts/);
+ expect(el[0].className).not.toMatch(/admin/);
+ $state.transitionTo('admin.roles', {page: 1});
+ $q.flush();
+ timeoutFlush();
+ expect(el[0].className).toMatch(/admin/);
+ expect(el[0].className).not.toMatch(/contacts/);
+ }));
+ });
});
describe('uiView controllers or onEnter handlers', function() {