Skip to content

Commit 46cdf4c

Browse files
fix(ng2.uiSrefStatus): calculate target state parameters
docs(ng2.uiSref): Basic documentation of uiSref/uiSrefActive
1 parent f28e0c3 commit 46cdf4c

File tree

5 files changed

+127
-31
lines changed

5 files changed

+127
-31
lines changed

src/common/common.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,11 @@ export const flatten = (arr: any[]) => arr.reduce(flattenR, []);
401401
* oneString.filter(assertPredicate(isNumber, "Not all numbers")); // throws Error(""Not all numbers"");
402402
* ```
403403
*/
404-
export function assertPredicate<T>(fn: Predicate<T>, errMsg: (string|Function) = "assert failure"): Predicate<T> {
404+
export function assertPredicate<T>(predicate: Predicate<T>, errMsg: (string|Function) = "assert failure"): Predicate<T> {
405405
return (obj: T) => {
406-
if (!fn(obj)) throw new Error(isFunction(errMsg) ? (<Function> errMsg)(obj) : errMsg);
406+
if (!predicate(obj)) {
407+
throw new Error(isFunction(errMsg) ? (<Function> errMsg)(obj) : errMsg);
408+
}
407409
return true;
408410
};
409411
}

src/ng2/uiSref.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/** @module ng2 */ /** */
22
import {UIRouter} from "../router";
3-
import {Directive, Inject} from "angular2/core";
3+
import {Directive, Inject, Input} from "angular2/core";
44
import {Optional} from "angular2/core";
55
import {ElementRef} from "angular2/core";
66
import {Renderer} from "angular2/core";
77
import {UiView} from "./uiView";
88
import {ViewContext} from "../view/interface";
99
import {extend} from "../common/common";
1010

11+
/** @hidden */
1112
@Directive({ selector: 'a[uiSref]' })
1213
export class AnchorUiSref {
1314
constructor(public _el: ElementRef, public _renderer: Renderer) { }
@@ -16,15 +17,39 @@ export class AnchorUiSref {
1617
}
1718
}
1819

20+
/**
21+
* A directive which, when clicked, begins a [[Transition]] to a [[TargetState]].
22+
*
23+
* Has three inputs:
24+
*
25+
* @Input uiSref the target state name
26+
*
27+
* @Input uiParams target state parameters
28+
*
29+
* @Input uiOptions transition options
30+
*
31+
* @example
32+
* ```html
33+
*
34+
* <!-- Targets bar state' -->
35+
* <a uiSref="bar">Bar</a>
36+
*
37+
* <!-- Assume this component's state is "foo".
38+
* Relatively targets "foo.child" -->
39+
* <a uiSref=".child">Foo Child</a>
40+
*
41+
* <!-- Targets "bar" state and supplies parameter value -->
42+
* <a uiSref="bar" [uiParams]="{ barId: foo.barId }">Bar {{foo.barId}}</a>
43+
* ```
44+
*/
1945
@Directive({
2046
selector: '[uiSref]',
21-
inputs: ['uiSref', 'uiParams', 'uiOptions'],
2247
host: { '(click)': 'go()' }
2348
})
2449
export class UiSref {
25-
state: string;
26-
params: any;
27-
options: any;
50+
@Input('uiSref') state: string;
51+
@Input('uiParams') params: any;
52+
@Input('uiOptions') options: any;
2853

2954
constructor(
3055
private _router: UIRouter,

src/ng2/uiSrefActive.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
21
import {Directive, Input, ElementRef, Host, Renderer} from "angular2/core";
32
import {UiSrefStatus, SrefStatus} from "./uiSrefStatus";
43

5-
@Directive({ selector: '[uiSrefActive],[uiSrefActiveEq]' })
4+
/**
5+
* A directive that pairs with a [[UiSref]] and adds a CSS classes when the state which the UiSref targets (or any
6+
* child state) is currently active.
7+
*
8+
* If the `uiSrefActiveEq` selector is used instead, the class is not added when a child state is active.
9+
*
10+
* @selector [uiSrefActive],[uiSrefActiveEq]
11+
*
12+
* @example
13+
* ```html
14+
*
15+
* <a uiSref="foo" uiSrefActive="active">Foo</a>
16+
* <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Foo Bar #{{bar.id}}</a>
17+
* ```
18+
*/
19+
@Directive({
20+
selector: '[uiSrefActive],[uiSrefActiveEq]'
21+
})
622
export class UiSrefActive {
23+
724
private _classes: string[] = [];
825
@Input('uiSrefActive') set active(val) { this._classes = val.split("\s+")};
926

@@ -17,4 +34,3 @@ export class UiSrefActive {
1734
});
1835
}
1936
}
20-

src/ng2/uiSrefStatus.ts

+62-17
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
import {Directive, Output, EventEmitter} from "angular2/core";
22
import {StateService} from "../state/stateService";
33
import {UiSref} from "./uiSref";
4-
import {UIRouter} from "../router";
54
import {Node} from "../path/node";
65
import {TransitionService} from "../transition/transitionService";
76
import {Transition} from "../transition/transition";
87
import {TargetState} from "../state/targetState";
98
import {TreeChanges} from "../transition/interface";
109
import {State} from "../state/stateObject";
11-
import {anyTrueR, tail} from "../common/common";
10+
import {anyTrueR, tail, unnestR} from "../common/common";
1211
import {UIRouterGlobals} from "../globals";
12+
import {Param} from "../params/param";
13+
import {PathFactory} from "../path/pathFactory";
1314

15+
/**
16+
* uiSref status booleans
17+
*/
1418
export interface SrefStatus {
19+
/** The sref's target state (or one of its children) is currently active */
1520
active: boolean;
21+
/** The sref's target state is currently active */
1622
exact: boolean;
23+
/** A transition is entering the sref's target state */
1724
entering: boolean;
25+
/** A transition is exiting the sref's target state */
1826
exiting: boolean;
1927
}
2028

2129
/**
22-
* Emits events when the uiSref status changes
30+
* A directive (which pairs with a [[UiSref]]) and emits events when the UiSref status changes.
31+
*
32+
* The event emitted is of type [[SrefStatus]], and has boolean values for `active`, `exact`, `entering`, and `exiting`
33+
*
34+
* The values from this event can be captured and stored on a component, then applied (perhaps using ngClass).
2335
*
2436
* This API is subject to change.
2537
*/
@@ -41,26 +53,29 @@ export class UiSrefStatus {
4153
private _globals: UIRouterGlobals,
4254
private _stateService: StateService,
4355
public sref: UiSref) {
44-
this._deregisterHook = transitionService.onStart({}, ($transition$) => this._transition($transition$));
56+
this._deregisterHook = transitionService.onStart({}, $transition$ => this.processTransition($transition$));
4557
}
4658

4759
ngOnInit() {
4860
let lastTrans = this._globals.transitionHistory.peekTail();
4961
if (lastTrans != null) {
50-
this._transition(lastTrans);
62+
this.processTransition(lastTrans);
5163
}
5264
}
5365

5466
ngOnDestroy() {
55-
this._deregisterHook()
67+
if (this._deregisterHook) {
68+
this._deregisterHook();
69+
}
70+
this._deregisterHook = null;
5671
}
5772

5873
private _setStatus(status: SrefStatus) {
5974
this.status = status;
6075
this.uiSrefStatus.emit(status);
6176
}
6277

63-
private _transition($transition$: Transition) {
78+
private processTransition($transition$: Transition) {
6479
let sref = this.sref;
6580

6681
let status: SrefStatus = <any> {
@@ -70,28 +85,58 @@ export class UiSrefStatus {
7085
exiting: false
7186
};
7287

73-
let srefTarget: TargetState = this._stateService.target(sref.state, sref.params, sref.options);
88+
let srefTarget: TargetState = this._stateService.target(sref.state, sref.params, sref.getOptions());
7489
if (!srefTarget.exists()) {
7590
return this._setStatus(status);
7691
}
7792

78-
let tc: TreeChanges = $transition$.treeChanges();
79-
let state: State = srefTarget.$state();
80-
const isTarget = (node: Node) => node.state === state;
8193

82-
status.active = tc.from.map(isTarget).reduce(anyTrueR, false);
83-
status.exact = tail(tc.from.map(isTarget)) === true;
84-
status.entering = tc.entering.map(isTarget).reduce(anyTrueR, false);
85-
status.exiting = tc.exiting.map(isTarget).reduce(anyTrueR, false);
94+
/**
95+
* Returns a Predicate<Node[]> that returns true when the target state (and any param values)
96+
* match the (tail of) the path, and the path's param values
97+
*/
98+
const pathMatches = (target: TargetState) => {
99+
let state: State = target.$state();
100+
let targetParamVals = target.params();
101+
let targetPath: Node[] = PathFactory.buildPath(target);
102+
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
103+
.reduce(unnestR, [])
104+
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));
105+
106+
return (path: Node[]) => {
107+
let tailNode = tail(path);
108+
if (!tailNode || tailNode.state !== state) return false;
109+
var paramValues = PathFactory.paramValues(path);
110+
return Param.equals(paramSchema, paramValues, targetParamVals);
111+
};
112+
};
113+
114+
const isTarget = pathMatches(srefTarget);
115+
116+
/**
117+
* Given path: [c, d] appendTo: [a, b]),
118+
* Expands the path to [c], [c, d]
119+
* Then appends each to [a,b,] and returns: [a, b, c], [a, b, c, d]
120+
*/
121+
function spreadToSubPaths (path: Node[], appendTo: Node[] = []): Node[][] {
122+
return path.map(node => appendTo.concat(PathFactory.subPath(path, node.state)));
123+
}
124+
125+
let tc: TreeChanges = $transition$.treeChanges();
126+
status.active = spreadToSubPaths(tc.from).map(isTarget).reduce(anyTrueR, false);
127+
status.exact = isTarget(tc.from);
128+
status.entering = spreadToSubPaths(tc.entering, tc.retained).map(isTarget).reduce(anyTrueR, false);
129+
status.exiting = spreadToSubPaths(tc.exiting, tc.retained).map(isTarget).reduce(anyTrueR, false);
86130

87131
if ($transition$.isActive()) {
88132
this._setStatus(status);
89133
}
90134

91135
let update = (currentPath: Node[]) => () => {
136+
if (this._deregisterHook == null) return; // destroyed
92137
if (!$transition$.isActive()) return; // superseded
93-
status.active = currentPath.map(isTarget).reduce(anyTrueR, false);
94-
status.exact = tail(currentPath.map(isTarget)) === true;
138+
status.active = spreadToSubPaths(currentPath).map(isTarget).reduce(anyTrueR, false);
139+
status.exact = isTarget(currentPath);
95140
status.entering = status.exiting = false;
96141
this._setStatus(status);
97142
};

src/path/pathFactory.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@ export class PathFactory {
2525
return new TargetState(state, state, path.map(prop("paramValues")).reduce(mergeR, {}));
2626
}
2727

28-
/** Given a fromPath: Node[] and a TargetState, builds a toPath: Node[] */
29-
static buildToPath(fromPath: Node[], targetState: TargetState): Node[] {
28+
static buildPath(targetState: TargetState) {
3029
let toParams = targetState.params();
31-
let toPath: Node[] = targetState.$state().path.map(state => new Node(state).applyRawParams(toParams));
30+
return targetState.$state().path.map(state => new Node(state).applyRawParams(toParams));
31+
}
3232

33-
if (targetState.options().inherit) toPath = PathFactory.inheritParams(fromPath, toPath, Object.keys(toParams));
33+
/** Given a fromPath: Node[] and a TargetState, builds a toPath: Node[] */
34+
static buildToPath(fromPath: Node[], targetState: TargetState): Node[] {
35+
let toPath: Node[] = PathFactory.buildPath(targetState);
36+
if (targetState.options().inherit) {
37+
return PathFactory.inheritParams(fromPath, toPath, Object.keys(targetState.params()));
38+
}
3439
return toPath;
3540
}
3641

@@ -159,4 +164,7 @@ export class PathFactory {
159164
if (elementIdx === -1) throw new Error("The path does not contain the state: " + state);
160165
return path.slice(0, elementIdx + 1);
161166
}
167+
168+
/** Gets the raw parameter values from a path */
169+
static paramValues = (path: Node[]) => path.reduce((acc, node) => extend(acc, node.paramValues), {});
162170
}

0 commit comments

Comments
 (0)