From a9ff6febb469e0d5cd49054216c4472df7a6259d Mon Sep 17 00:00:00 2001 From: Francesco Pipita Date: Sun, 8 Nov 2015 00:20:18 +0100 Subject: [PATCH] feat(uiSrefActive): provide a ng-{class,style} like interface resolves #1431 --- src/common.js | 3 +- src/stateDirectives.js | 78 ++++++++++++++++++++++++++++++++----- test/stateDirectivesSpec.js | 53 +++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 11 deletions(-) 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..964ddc420 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: + *
+ * 
+ * Roles + *
+ *
+ * + * 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)) { 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('
Roles
')($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() {