Skip to content

Commit 874fc07

Browse files
fix(ng2.uiSrefActive): Allow uiSrefActive on ancestor element.
feat(ng2.uiSrefActive): Refactor using observables Closes #2950
1 parent 2a2f381 commit 874fc07

File tree

3 files changed

+162
-105
lines changed

3 files changed

+162
-105
lines changed

src/ng2/directives/uiSref.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {Renderer} from "@angular/core";
77
import {UIView, ParentUIViewInject} from "./uiView";
88
import {extend, Obj} from "../../common/common";
99
import {TransitionOptions} from "../../transition/interface";
10+
import {Globals, UIRouterGlobals} from "../../globals";
11+
import {Subscription, ReplaySubject} from "rxjs/Rx";
12+
import {TargetState} from "../../state/targetState";
1013

1114
/** @hidden */
1215
@Directive({ selector: 'a[uiSref]' })
@@ -67,39 +70,58 @@ export class UISref {
6770
@Input('uiParams') params: any;
6871
@Input('uiOptions') options: any;
6972

73+
public targetState$ = new ReplaySubject<TargetState>(1);
74+
private _emit: boolean = false;
75+
76+
private _statesSub: Subscription;
77+
7078
constructor(
7179
private _router: UIRouter,
7280
@Inject(UIView.PARENT_INJECT) public parent: ParentUIViewInject,
73-
@Optional() private _anchorUISref: AnchorUISref
74-
) { }
81+
@Optional() private _anchorUISref: AnchorUISref,
82+
@Inject(Globals) _globals: UIRouterGlobals
83+
) {
84+
this._statesSub = _globals.states$.subscribe(() => this.update())
85+
}
7586

7687
set "uiSref"(val: string) { this.state = val; this.update(); }
7788
set "uiParams"(val: Obj) { this.params = val; this.update(); }
7889
set "uiOptions"(val: TransitionOptions) { this.options = val; this.update(); }
7990

8091
ngOnInit() {
92+
this._emit = true;
8193
this.update();
8294
}
8395

96+
ngOnDestroy() {
97+
this._statesSub.unsubscribe();
98+
this.targetState$.unsubscribe();
99+
}
100+
84101
update() {
102+
let $state = this._router.stateService;
103+
if (this._emit) {
104+
let newTarget = $state.target(this.state, this.params, this.getOptions());
105+
this.targetState$.next(newTarget);
106+
}
107+
85108
if (this._anchorUISref) {
86-
this._anchorUISref.update(this._router.stateService.href(this.state, this.params, this.getOptions()));
109+
let href = $state.href(this.state, this.params, this.getOptions());
110+
this._anchorUISref.update(href);
87111
}
88112
}
89113

90114
getOptions() {
91-
let defOpts: TransitionOptions = {
115+
let defaultOpts: TransitionOptions = {
92116
relative: this.parent && this.parent.context && this.parent.context.name,
93117
inherit: true ,
94118
source: "sref"
95119
};
96-
return extend(defOpts, this.options || {});
120+
return extend(defaultOpts, this.options || {});
97121
}
98122

99123
go() {
100124
this._router.stateService.go(this.state, this.params, this.getOptions());
101125
return false;
102126
}
103127
}
104-
105-

src/ng2/directives/uiSrefActive.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @module ng2_directives */ /** */
22
import {Directive, Input, ElementRef, Host, Renderer} from "@angular/core";
33
import {UISrefStatus, SrefStatus} from "./uiSrefStatus";
4+
import {Subscription} from "rxjs/Rx";
45

56
/**
67
* A directive that adds a CSS class when a `uiSref` is active.
@@ -38,10 +39,15 @@ export class UISrefActive {
3839
private _classesEq: string[] = [];
3940
@Input('uiSrefActiveEq') set activeEq(val: string) { this._classesEq = val.split("\s+")};
4041

42+
private _subscription: Subscription;
4143
constructor(uiSrefStatus: UISrefStatus, rnd: Renderer, @Host() host: ElementRef) {
42-
uiSrefStatus.uiSrefStatus.subscribe((next: SrefStatus) => {
44+
this._subscription = uiSrefStatus.uiSrefStatus.subscribe((next: SrefStatus) => {
4345
this._classes.forEach(cls => rnd.setElementClass(host.nativeElement, cls, next.active));
4446
this._classesEq.forEach(cls => rnd.setElementClass(host.nativeElement, cls, next.exact));
4547
});
4648
}
49+
50+
ngOnDestroy() {
51+
this._subscription.unsubscribe();
52+
}
4753
}

src/ng2/directives/uiSrefStatus.ts

+126-97
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/** @module ng2_directives */ /** */
2-
import {Directive, Output, EventEmitter, ContentChild} from "@angular/core";
3-
import {StateService} from "../../state/stateService";
2+
import {Directive, Output, EventEmitter, ContentChildren, QueryList, Inject} from "@angular/core";
43
import {UISref} from "./uiSref";
54
import {PathNode} from "../../path/node";
6-
import {TransitionService} from "../../transition/transitionService";
75
import {Transition} from "../../transition/transition";
86
import {TargetState} from "../../state/targetState";
9-
import {TreeChanges} from "../../transition/interface";
107
import {State} from "../../state/stateObject";
11-
import {anyTrueR, tail, unnestR} from "../../common/common";
12-
import {Globals} from "../../globals";
8+
import {anyTrueR, tail, unnestR, Predicate} from "../../common/common";
9+
import {Globals, UIRouterGlobals} from "../../globals";
1310
import {Param} from "../../params/param";
1411
import {PathFactory} from "../../path/pathFactory";
12+
import {Subscription, Observable} from "rxjs/Rx";
13+
14+
interface TransEvt { evt: string, trans: Transition }
1515

1616
/**
1717
* uiSref status booleans
@@ -27,6 +27,84 @@ export interface SrefStatus {
2727
exiting: boolean;
2828
}
2929

30+
const inactiveStatus: SrefStatus = {
31+
active: false,
32+
exact: false,
33+
entering: false,
34+
exiting: false
35+
};
36+
37+
/**
38+
* Returns a Predicate<PathNode[]>
39+
*
40+
* The predicate returns true when the target state (and param values)
41+
* match the (tail of) the path, and the path's param values
42+
*/
43+
const pathMatches = (target: TargetState): Predicate<PathNode[]> => {
44+
let state: State = target.$state();
45+
let targetParamVals = target.params();
46+
let targetPath: PathNode[] = PathFactory.buildPath(target);
47+
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
48+
.reduce(unnestR, [])
49+
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));
50+
51+
return (path: PathNode[]) => {
52+
let tailNode = tail(path);
53+
if (!tailNode || tailNode.state !== state) return false;
54+
var paramValues = PathFactory.paramValues(path);
55+
return Param.equals(paramSchema, paramValues, targetParamVals);
56+
};
57+
};
58+
59+
/**
60+
* Given basePath: [a, b], appendPath: [c, d]),
61+
* Expands the path to [c], [c, d]
62+
* Then appends each to [a,b,] and returns: [a, b, c], [a, b, c, d]
63+
*/
64+
function spreadToSubPaths(basePath: PathNode[], appendPath: PathNode[]): PathNode[][] {
65+
return appendPath.map(node => basePath.concat(PathFactory.subPath(appendPath, n => n.state === node.state)));
66+
}
67+
68+
/**
69+
* Given a TransEvt (Transition event: started, success, error)
70+
* and a UISref Target State, return a SrefStatus object
71+
* which represents the current status of that Sref:
72+
* active, activeEq (exact match), entering, exiting
73+
*/
74+
function getSrefStatus(event: TransEvt, srefTarget: TargetState): SrefStatus {
75+
const pathMatchesTarget = pathMatches(srefTarget);
76+
const tc = event.trans.treeChanges();
77+
78+
let isStartEvent = event.evt === 'start';
79+
let isSuccessEvent = event.evt === 'success';
80+
let activePath: PathNode[] = isSuccessEvent ? tc.to : tc.from;
81+
82+
const isActive = () =>
83+
spreadToSubPaths([], activePath)
84+
.map(pathMatchesTarget)
85+
.reduce(anyTrueR, false);
86+
87+
const isExact = () =>
88+
pathMatchesTarget(activePath);
89+
90+
const isEntering = () =>
91+
spreadToSubPaths(tc.retained, tc.entering)
92+
.map(pathMatchesTarget)
93+
.reduce(anyTrueR, false);
94+
95+
const isExiting = () =>
96+
spreadToSubPaths(tc.retained, tc.exiting)
97+
.map(pathMatchesTarget)
98+
.reduce(anyTrueR, false);
99+
100+
return {
101+
active: isActive(),
102+
exact: isExact(),
103+
entering: isStartEvent ? isEntering() : false,
104+
exiting: isStartEvent ? isExiting() : false,
105+
} as SrefStatus;
106+
}
107+
30108
/**
31109
* A directive (which pairs with a [[UISref]]) and emits events when the UISref status changes.
32110
*
@@ -40,110 +118,61 @@ export interface SrefStatus {
40118
*/
41119
@Directive({ selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]' })
42120
export class UISrefStatus {
43-
private _deregisterHook: Function;
44-
45-
// current statuses of the state/params the uiSref directive is linking to
121+
/** current statuses of the state/params the uiSref directive is linking to */
46122
@Output("uiSrefStatus") uiSrefStatus = new EventEmitter<SrefStatus>(false);
47-
@ContentChild(UISref) sref: UISref;
123+
/** Monitor all child components for UISref(s) */
124+
@ContentChildren(UISref, {descendants: true}) srefs: QueryList<UISref>;
48125

49-
status: SrefStatus = {
50-
active: false,
51-
exact: false,
52-
entering: false,
53-
exiting: false
54-
};
126+
/** The current status */
127+
status: SrefStatus;
128+
129+
private _subscription: Subscription;
55130

56-
constructor(transitionService: TransitionService,
57-
private _globals: Globals,
58-
private _stateService: StateService) {
59-
this._deregisterHook = transitionService.onStart({}, $transition$ => this.processTransition($transition$));
131+
constructor(@Inject(Globals) private _globals: UIRouterGlobals) {
132+
this.status = Object.assign({}, inactiveStatus);
60133
}
61134

62135
ngAfterContentInit() {
63-
let lastTrans = this._globals.transitionHistory.peekTail();
64-
if (lastTrans != null) {
65-
this.processTransition(lastTrans);
66-
}
136+
// Map each transition start event to a stream of:
137+
// start -> (success|error)
138+
let transEvents$: Observable<TransEvt> = this._globals.start$.switchMap((trans: Transition) => {
139+
const event = (evt: string) => ({evt, trans} as TransEvt);
140+
141+
let transStart$ = Observable.of(event("start"));
142+
let transResult = trans.promise.then(() => event("success"), () => event("error"));
143+
let transFinish$ = Observable.fromPromise(transResult);
144+
145+
return transStart$.concat(transFinish$);
146+
});
147+
148+
// Watch the children UISref components and get their target states
149+
let srefs$: Observable<UISref[]> = Observable.of(this.srefs.toArray()).concat(this.srefs.changes);
150+
let targetStates$: Observable<TargetState[]> =
151+
srefs$.switchMap((srefs: UISref[]) =>
152+
Observable.combineLatest<TargetState[]>(srefs.map(sref => sref.targetState$)));
153+
154+
// Calculate the status of each UISref based on the transition event.
155+
// Reduce the statuses (if multiple) by or-ing each flag.
156+
this._subscription = transEvents$.mergeMap((evt: TransEvt) => {
157+
return targetStates$.map((targets: TargetState[]) => {
158+
let statuses: SrefStatus[] = targets.map(target => getSrefStatus(evt, target));
159+
160+
return statuses.reduce((acc: SrefStatus, val: SrefStatus) => ({
161+
active: acc.active || val.active,
162+
exact: acc.active || val.active,
163+
entering: acc.active || val.active,
164+
exiting: acc.active || val.active,
165+
}))
166+
})
167+
}).subscribe(this._setStatus.bind(this));
67168
}
68169

69170
ngOnDestroy() {
70-
if (this._deregisterHook) {
71-
this._deregisterHook();
72-
}
73-
this._deregisterHook = null;
171+
if (this._subscription) this._subscription.unsubscribe();
74172
}
75173

76174
private _setStatus(status: SrefStatus) {
77175
this.status = status;
78176
this.uiSrefStatus.emit(status);
79177
}
80-
81-
private processTransition($transition$: Transition) {
82-
let sref = this.sref;
83-
84-
let status: SrefStatus = <any> {
85-
active: false,
86-
exact: false,
87-
entering: false,
88-
exiting: false
89-
};
90-
91-
let srefTarget: TargetState = this._stateService.target(sref.state, sref.params, sref.getOptions());
92-
if (!srefTarget.exists()) {
93-
return this._setStatus(status);
94-
}
95-
96-
97-
/**
98-
* Returns a Predicate<PathNode[]> that returns true when the target state (and any param values)
99-
* match the (tail of) the path, and the path's param values
100-
*/
101-
const pathMatches = (target: TargetState) => {
102-
let state: State = target.$state();
103-
let targetParamVals = target.params();
104-
let targetPath: PathNode[] = PathFactory.buildPath(target);
105-
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
106-
.reduce(unnestR, [])
107-
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));
108-
109-
return (path: PathNode[]) => {
110-
let tailNode = tail(path);
111-
if (!tailNode || tailNode.state !== state) return false;
112-
var paramValues = PathFactory.paramValues(path);
113-
return Param.equals(paramSchema, paramValues, targetParamVals);
114-
};
115-
};
116-
117-
const isTarget = pathMatches(srefTarget);
118-
119-
/**
120-
* Given path: [c, d] appendTo: [a, b]),
121-
* Expands the path to [c], [c, d]
122-
* Then appends each to [a,b,] and returns: [a, b, c], [a, b, c, d]
123-
*/
124-
function spreadToSubPaths (path: PathNode[], appendTo: PathNode[] = []): PathNode[][] {
125-
return path.map(node => appendTo.concat(PathFactory.subPath(path, n => n.state === node.state)));
126-
}
127-
128-
let tc: TreeChanges = $transition$.treeChanges();
129-
status.active = spreadToSubPaths(tc.from).map(isTarget).reduce(anyTrueR, false);
130-
status.exact = isTarget(tc.from);
131-
status.entering = spreadToSubPaths(tc.entering, tc.retained).map(isTarget).reduce(anyTrueR, false);
132-
status.exiting = spreadToSubPaths(tc.exiting, tc.retained).map(isTarget).reduce(anyTrueR, false);
133-
134-
if ($transition$.isActive()) {
135-
this._setStatus(status);
136-
}
137-
138-
let update = (currentPath: PathNode[]) => () => {
139-
if (this._deregisterHook == null) return; // destroyed
140-
if (!$transition$.isActive()) return; // superseded
141-
status.active = spreadToSubPaths(currentPath).map(isTarget).reduce(anyTrueR, false);
142-
status.exact = isTarget(currentPath);
143-
status.entering = status.exiting = false;
144-
this._setStatus(status);
145-
};
146-
147-
$transition$.promise.then(update(tc.to), update(tc.from));
148-
}
149178
}

0 commit comments

Comments
 (0)