Skip to content

Commit 126a4ad

Browse files
feat(ui-sref-active): improve performance by reducing $watches
fix(ui-sref-active): Avoid add/remove class race condition refactor(ui-sref-active): Apply all CSS changes in a single `$evalAsync` See also: 6a9d9ae #1997 #2503 #1997 #2012 Closes #2777
1 parent 6a9d9ae commit 126a4ad

File tree

1 file changed

+32
-63
lines changed

1 file changed

+32
-63
lines changed

src/ng1/directives/stateDirectives.ts

+32-63
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { ng as angular } from "../../angular";
99
import { IAugmentedJQuery, ITimeoutService, IScope, IInterpolateService } from "angular";
1010

1111
import {
12-
Obj, extend, forEach, toJson, tail, isString, isObject, parse, noop,
13-
PathNode, StateOrName, StateService, TransitionService, State, UIRouter
12+
Obj, extend, forEach, tail, isString, isObject, parse, noop, unnestR, identity, uniqR, inArray, removeFrom,
13+
RawParams, PathNode, StateOrName, StateService, TransitionService, StateDeclaration, UIRouter
1414
} from "ui-router-core";
1515
import { UIViewData } from "./viewDirective";
1616

@@ -354,13 +354,15 @@ let uiState = ['$uiRouter', '$timeout',
354354
* to both the <div> and <a> elements. It is important to note that the state
355355
* names/globs passed to ui-sref-active shadow the state provided by ui-sref.
356356
*/
357-
let uiSrefActive = ['$state', '$stateParams', '$interpolate', '$transitions', '$uiRouter',
358-
function $StateRefActiveDirective($state: StateService, $stateParams: Obj, $interpolate: IInterpolateService, $transitions: TransitionService, $uiRouter: UIRouter) {
357+
let uiSrefActive = ['$state', '$stateParams', '$interpolate', '$uiRouter',
358+
function $StateRefActiveDirective($state: StateService, $stateParams: Obj, $interpolate: IInterpolateService, $uiRouter: UIRouter) {
359359
return {
360360
restrict: "A",
361-
controller: ['$scope', '$element', '$attrs', '$timeout',
362-
function ($scope: IScope, $element: IAugmentedJQuery, $attrs: any, $timeout: ITimeoutService) {
363-
var states: any[] = [], activeClasses: Obj = {}, activeEqClass: string, uiSrefActive: any;
361+
controller: ['$scope', '$element', '$attrs',
362+
function ($scope: IScope, $element: IAugmentedJQuery, $attrs: any) {
363+
var states: StateData[] = [],
364+
activeEqClass: string,
365+
uiSrefActive: any;
364366

365367
// There probably isn't much point in $observing this
366368
// uiSrefActive and uiSrefActiveEq share the same directive object with some
@@ -400,89 +402,56 @@ let uiSrefActive = ['$state', '$stateParams', '$interpolate', '$transitions', '$
400402
}
401403

402404
$scope.$on('$stateChangeSuccess', update);
403-
$scope.$on('$destroy', <any> $transitions.onStart({}, updateAfterTransition));
405+
$scope.$on('$destroy', <any> $uiRouter.transitionService.onStart({}, updateAfterTransition));
404406
if ($uiRouter.globals.transition) {
405407
updateAfterTransition($uiRouter.globals.transition);
406408
}
407409

408410
function addState(stateName: string, stateParams: Obj, activeClass: string) {
409411
var state = $state.get(stateName, stateContext($element));
410-
var stateHash = createStateHash(stateName, stateParams);
411412

412413
var stateInfo = {
413414
state: state || { name: stateName },
414415
params: stateParams,
415-
hash: stateHash
416+
activeClass: activeClass
416417
};
417418

418419
states.push(stateInfo);
419-
activeClasses[stateHash] = activeClass;
420420

421421
return function removeState() {
422-
var idx = states.indexOf(stateInfo);
423-
if (idx !== -1) states.splice(idx, 1);
422+
removeFrom(states)(stateInfo);
424423
}
425424
}
426425

427-
/**
428-
* @param {string} state
429-
* @param {Object|string} [params]
430-
* @return {string}
431-
*/
432-
function createStateHash(state: string, params: (Obj|string)) {
433-
if (!isString(state)) {
434-
throw new Error('state should be a string');
435-
}
436-
if (isObject(params)) {
437-
return state + toJson(params);
438-
}
439-
params = $scope.$eval(params as string);
440-
if (isObject(params)) {
441-
return state + toJson(params);
442-
}
443-
return state;
444-
}
445-
446426
// Update route state
447427
function update() {
448-
for (var i = 0; i < states.length; i++) {
449-
if (anyMatch(states[i].state, states[i].params)) {
450-
addClass($element, activeClasses[states[i].hash]);
451-
} else {
452-
removeClass($element, activeClasses[states[i].hash]);
453-
}
454-
455-
if (exactMatch(states[i].state, states[i].params)) {
456-
addClass($element, activeEqClass);
457-
} else {
458-
removeClass($element, activeEqClass);
459-
}
460-
}
461-
}
462-
463-
function addClass(el: IAugmentedJQuery, className: string) {
464-
$scope.$evalAsync(() => el.addClass(className));
465-
}
466-
467-
function removeClass(el: IAugmentedJQuery, className: string) {
468-
$scope.$evalAsync(() => el.removeClass(className));
469-
}
470-
471-
function anyMatch(state: State, params: Obj) {
472-
return $state.includes(state.name, params);
473-
}
474-
475-
function exactMatch(state: State, params: Obj) {
476-
return $state.is(state.name, params);
428+
const splitClasses = str =>
429+
str.split(/\s/).filter(identity);
430+
const getClasses = (stateList: StateData[]) =>
431+
stateList.map(x => x.activeClass).map(splitClasses).reduce(unnestR, []);
432+
433+
let allClasses = getClasses(states).concat(splitClasses(activeEqClass)).reduce(uniqR, []);
434+
let fuzzyClasses = getClasses(states.filter(x => $state.includes(x.state.name, x.params)));
435+
let exactlyMatchesAny = !!states.filter(x => $state.is(x.state.name, x.params)).length;
436+
let exactClasses = exactlyMatchesAny ? splitClasses(activeEqClass) : [];
437+
438+
let addClasses = fuzzyClasses.concat(exactClasses).reduce(uniqR, []);
439+
let removeClasses = allClasses.filter(cls => !inArray(addClasses, cls));
440+
441+
$scope.$evalAsync(() => {
442+
addClasses.forEach(className => $element.addClass(className));
443+
removeClasses.forEach(className => $element.removeClass(className));
444+
});
477445
}
478446

479447
update();
480448
}]
481449
};
482450
}];
483451

484-
interface Def { uiState: string; href: string; uiStateParams: Obj; uiStateOpts: any;
485-
}
452+
interface Def { uiState: string; href: string; uiStateParams: Obj; uiStateOpts: any; }
453+
interface StateData { state: StateDeclaration; params: RawParams; activeClass: string; }
454+
486455
angular.module('ui.router.state')
487456
.directive('uiSref', uiSref)
488457
.directive('uiSrefActive', uiSrefActive)

0 commit comments

Comments
 (0)