Skip to content

Commit 4398690

Browse files
Merge pull request #2375 from fpipita/feature-1.0
feat(uiSrefActive): provide a ng-{class,style} like interface
2 parents 141d7b0 + eeec65b commit 4398690

File tree

2 files changed

+118
-10
lines changed

2 files changed

+118
-10
lines changed

src/state/stateDirectives.ts

+65-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// <reference path='../../typings/angularjs/angular.d.ts' />
2-
import {copy, defaults} from "../common/common";
2+
import {copy, defaults, isString, isObject, forEach, toJson} from "../common/common";
33
import {defaultTransOpts} from "../transition/transitionService";
44

55
function parseStateRef(ref, current) {
@@ -199,6 +199,24 @@ function $StateRefDirective($state, $timeout) {
199199
* </li>
200200
* </ul>
201201
* </pre>
202+
*
203+
* It is also possible to pass ui-sref-active an expression that evaluates
204+
* to an object hash, whose keys represent active class names and whose
205+
* values represent the respective state names/globs.
206+
* ui-sref-active will match if the current active state **includes** any of
207+
* the specified state names/globs, even the abstract ones.
208+
*
209+
* @Example
210+
* Given the following template, with "admin" being an abstract state:
211+
* <pre>
212+
* <div ui-sref-active="{'active': 'admin.*'}">
213+
* <a ui-sref-active="active" ui-sref="admin.roles">Roles</a>
214+
* </div>
215+
* </pre>
216+
*
217+
* When the current state is "admin.roles" the "active" class will be applied
218+
* to both the <div> and <a> elements. It is important to note that the state
219+
* names/globs passed to ui-sref-active shadow the state provided by ui-sref.
202220
*/
203221

204222
/**
@@ -221,37 +239,74 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
221239
return {
222240
restrict: "A",
223241
controller: ['$scope', '$element', '$attrs', '$timeout', '$transitions', function ($scope, $element, $attrs, $timeout, $transitions) {
224-
let states = [], activeClass, activeEqClass;
242+
let states = [], activeClasses = {}, activeEqClass;
225243

226244
// There probably isn't much point in $observing this
227245
// uiSrefActive and uiSrefActiveEq share the same directive object with some
228246
// slight difference in logic routing
229-
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
230247
activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope);
231248

249+
var uiSrefActive = $scope.$eval($attrs.uiSrefActive) || $interpolate($attrs.uiSrefActive || '', false)($scope);
250+
if (isObject(uiSrefActive)) {
251+
forEach(uiSrefActive, function(stateOrName, activeClass) {
252+
if (isString(stateOrName)) {
253+
var ref = parseStateRef(stateOrName, $state.current.name);
254+
addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
255+
}
256+
});
257+
}
258+
232259
// Allow uiSref to communicate with uiSrefActive[Equals]
233260
this.$$addStateInfo = function (newState, newParams) {
234-
let state = $state.get(newState, stateContext($element));
261+
// we already got an explicit state provided by ui-sref-active, so we
262+
// shadow the one that comes from ui-sref
263+
if (isObject(uiSrefActive) && states.length > 0) {
264+
return;
265+
}
266+
addState(newState, newParams, uiSrefActive);
267+
update();
268+
};
269+
270+
$scope.$on('$stateChangeSuccess', update);
271+
272+
function addState(stateName, stateParams, activeClass) {
273+
var state = $state.get(stateName, stateContext($element));
274+
var stateHash = createStateHash(stateName, stateParams);
235275

236276
states.push({
237-
state: state || { name: newState },
238-
params: newParams
277+
state: state || { name: stateName },
278+
params: stateParams,
279+
hash: stateHash
239280
});
240281

241-
update();
242-
};
282+
activeClasses[stateHash] = activeClass;
283+
}
243284

244285
let updateAfterTransition = function ($transition$) { $transition$.promise.then(update); };
245286
let deregisterFn = $transitions.onStart({}, updateAfterTransition);
246287
$scope.$on('$destroy', deregisterFn);
247288

289+
function createStateHash(state, params) {
290+
if (!isString(state)) {
291+
throw new Error('state should be a string');
292+
}
293+
if (isObject(params)) {
294+
return state + toJson(params);
295+
}
296+
params = $scope.$eval(params);
297+
if (isObject(params)) {
298+
return state + toJson(params);
299+
}
300+
return state;
301+
}
302+
248303
// Update route state
249304
function update() {
250305
for (let i = 0; i < states.length; i++) {
251306
if (anyMatch(states[i].state, states[i].params)) {
252-
addClass($element, activeClass);
307+
addClass($element, activeClasses[states[i].hash]);
253308
} else {
254-
removeClass($element, activeClass);
309+
removeClass($element, activeClasses[states[i].hash]);
255310
}
256311

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

test/stateDirectivesSpec.js

+53
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ describe('uiSrefActive', function() {
424424
url: '/detail/:foo'
425425
}).state('contacts.item.edit', {
426426
url: '/edit'
427+
}).state('admin', {
428+
url: '/admin',
429+
abstract: true,
430+
template: '<ui-view/>'
431+
}).state('admin.roles', {
432+
url: '/roles?page'
427433
});
428434
}));
429435

@@ -609,4 +615,51 @@ describe('uiSrefActive', function() {
609615
timeoutFlush();
610616
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
611617
}));
618+
619+
describe('ng-{class,style} interface', function() {
620+
it('should match on abstract states that are included by the current state', inject(function($rootScope, $compile, $state, $q) {
621+
el = $compile('<div ui-sref-active="{active: \'admin.*\'}"><a ui-sref-active="active" ui-sref="admin.roles">Roles</a></div>')($rootScope);
622+
$state.transitionTo('admin.roles');
623+
$q.flush();
624+
timeoutFlush();
625+
var abstractParent = el[0];
626+
expect(abstractParent.className).toMatch(/active/);
627+
var child = el[0].querySelector('a');
628+
expect(child.className).toMatch(/active/);
629+
}));
630+
631+
it('should match on state parameters', inject(function($compile, $rootScope, $state, $q) {
632+
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"></div>')($rootScope);
633+
$state.transitionTo('admin.roles', {page: 1});
634+
$q.flush();
635+
timeoutFlush();
636+
expect(el[0].className).toMatch(/active/);
637+
}));
638+
639+
it('should shadow the state provided by ui-sref', inject(function($compile, $rootScope, $state, $q) {
640+
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"><a ui-sref="admin.roles"></a></div>')($rootScope);
641+
$state.transitionTo('admin.roles');
642+
$q.flush();
643+
timeoutFlush();
644+
expect(el[0].className).not.toMatch(/active/);
645+
$state.transitionTo('admin.roles', {page: 1});
646+
$q.flush();
647+
timeoutFlush();
648+
expect(el[0].className).toMatch(/active/);
649+
}));
650+
651+
it('should support multiple <className, stateOrName> pairs', inject(function($compile, $rootScope, $state, $q) {
652+
el = $compile('<div ui-sref-active="{contacts: \'contacts.*\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
653+
$state.transitionTo('contacts');
654+
$q.flush();
655+
timeoutFlush();
656+
expect(el[0].className).toMatch(/contacts/);
657+
expect(el[0].className).not.toMatch(/admin/);
658+
$state.transitionTo('admin.roles', {page: 1});
659+
$q.flush();
660+
timeoutFlush();
661+
expect(el[0].className).toMatch(/admin/);
662+
expect(el[0].className).not.toMatch(/contacts/);
663+
}));
664+
});
612665
});

0 commit comments

Comments
 (0)