diff --git a/src/state/stateDirectives.ts b/src/state/stateDirectives.ts
index ac8707a33..0806f7771 100644
--- a/src/state/stateDirectives.ts
+++ b/src/state/stateDirectives.ts
@@ -1,5 +1,5 @@
///
-import {copy, defaults} from "../common/common";
+import {copy, defaults, isString, isObject, forEach, toJson} from "../common/common";
import {defaultTransOpts} from "../transition/transitionService";
function parseStateRef(ref, current) {
@@ -199,6 +199,24 @@ function $StateRefDirective($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.
*/
/**
@@ -221,37 +239,74 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', '$timeout', '$transitions', function ($scope, $element, $attrs, $timeout, $transitions) {
- let states = [], activeClass, activeEqClass;
+ let 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) {
- let 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;
+ }
let updateAfterTransition = function ($transition$) { $transition$.promise.then(update); };
let deregisterFn = $transitions.onStart({}, updateAfterTransition);
$scope.$on('$destroy', deregisterFn);
+ 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 (let 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)) {
diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js
index 03b1c92f2..48778bd07 100644
--- a/test/stateDirectivesSpec.js
+++ b/test/stateDirectivesSpec.js
@@ -424,6 +424,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'
});
}));
@@ -609,4 +615,51 @@ 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/);
+ }));
+ });
});