Skip to content

Commit

Permalink
fix(ng2.uiSrefStatus): calculate target state parameters
Browse files Browse the repository at this point in the history
docs(ng2.uiSref): Basic documentation of uiSref/uiSrefActive
  • Loading branch information
christopherthielen committed Apr 3, 2016
1 parent f28e0c3 commit 46cdf4c
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 31 deletions.
6 changes: 4 additions & 2 deletions src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,11 @@ export const flatten = (arr: any[]) => arr.reduce(flattenR, []);
* oneString.filter(assertPredicate(isNumber, "Not all numbers")); // throws Error(""Not all numbers"");
* ```
*/
export function assertPredicate<T>(fn: Predicate<T>, errMsg: (string|Function) = "assert failure"): Predicate<T> {
export function assertPredicate<T>(predicate: Predicate<T>, errMsg: (string|Function) = "assert failure"): Predicate<T> {
return (obj: T) => {
if (!fn(obj)) throw new Error(isFunction(errMsg) ? (<Function> errMsg)(obj) : errMsg);
if (!predicate(obj)) {
throw new Error(isFunction(errMsg) ? (<Function> errMsg)(obj) : errMsg);
}
return true;
};
}
Expand Down
35 changes: 30 additions & 5 deletions src/ng2/uiSref.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/** @module ng2 */ /** */
import {UIRouter} from "../router";
import {Directive, Inject} from "angular2/core";
import {Directive, Inject, Input} from "angular2/core";
import {Optional} from "angular2/core";
import {ElementRef} from "angular2/core";
import {Renderer} from "angular2/core";
import {UiView} from "./uiView";
import {ViewContext} from "../view/interface";
import {extend} from "../common/common";

/** @hidden */
@Directive({ selector: 'a[uiSref]' })
export class AnchorUiSref {
constructor(public _el: ElementRef, public _renderer: Renderer) { }
Expand All @@ -16,15 +17,39 @@ export class AnchorUiSref {
}
}

/**
* A directive which, when clicked, begins a [[Transition]] to a [[TargetState]].
*
* Has three inputs:
*
* @Input uiSref the target state name
*
* @Input uiParams target state parameters
*
* @Input uiOptions transition options
*
* @example
* ```html
*
* <!-- Targets bar state' -->
* <a uiSref="bar">Bar</a>
*
* <!-- Assume this component's state is "foo".
* Relatively targets "foo.child" -->
* <a uiSref=".child">Foo Child</a>
*
* <!-- Targets "bar" state and supplies parameter value -->
* <a uiSref="bar" [uiParams]="{ barId: foo.barId }">Bar {{foo.barId}}</a>
* ```
*/
@Directive({
selector: '[uiSref]',
inputs: ['uiSref', 'uiParams', 'uiOptions'],
host: { '(click)': 'go()' }
})
export class UiSref {
state: string;
params: any;
options: any;
@Input('uiSref') state: string;
@Input('uiParams') params: any;
@Input('uiOptions') options: any;

constructor(
private _router: UIRouter,
Expand Down
22 changes: 19 additions & 3 deletions src/ng2/uiSrefActive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@

import {Directive, Input, ElementRef, Host, Renderer} from "angular2/core";
import {UiSrefStatus, SrefStatus} from "./uiSrefStatus";

@Directive({ selector: '[uiSrefActive],[uiSrefActiveEq]' })
/**
* A directive that pairs with a [[UiSref]] and adds a CSS classes when the state which the UiSref targets (or any
* child state) is currently active.
*
* If the `uiSrefActiveEq` selector is used instead, the class is not added when a child state is active.
*
* @selector [uiSrefActive],[uiSrefActiveEq]
*
* @example
* ```html
*
* <a uiSref="foo" uiSrefActive="active">Foo</a>
* <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Foo Bar #{{bar.id}}</a>
* ```
*/
@Directive({
selector: '[uiSrefActive],[uiSrefActiveEq]'
})
export class UiSrefActive {

private _classes: string[] = [];
@Input('uiSrefActive') set active(val) { this._classes = val.split("\s+")};

Expand All @@ -17,4 +34,3 @@ export class UiSrefActive {
});
}
}

79 changes: 62 additions & 17 deletions src/ng2/uiSrefStatus.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
import {Directive, Output, EventEmitter} from "angular2/core";
import {StateService} from "../state/stateService";
import {UiSref} from "./uiSref";
import {UIRouter} from "../router";
import {Node} from "../path/node";
import {TransitionService} from "../transition/transitionService";
import {Transition} from "../transition/transition";
import {TargetState} from "../state/targetState";
import {TreeChanges} from "../transition/interface";
import {State} from "../state/stateObject";
import {anyTrueR, tail} from "../common/common";
import {anyTrueR, tail, unnestR} from "../common/common";
import {UIRouterGlobals} from "../globals";
import {Param} from "../params/param";
import {PathFactory} from "../path/pathFactory";

/**
* uiSref status booleans
*/
export interface SrefStatus {
/** The sref's target state (or one of its children) is currently active */
active: boolean;
/** The sref's target state is currently active */
exact: boolean;
/** A transition is entering the sref's target state */
entering: boolean;
/** A transition is exiting the sref's target state */
exiting: boolean;
}

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

ngOnInit() {
let lastTrans = this._globals.transitionHistory.peekTail();
if (lastTrans != null) {
this._transition(lastTrans);
this.processTransition(lastTrans);
}
}

ngOnDestroy() {
this._deregisterHook()
if (this._deregisterHook) {
this._deregisterHook();
}
this._deregisterHook = null;
}

private _setStatus(status: SrefStatus) {
this.status = status;
this.uiSrefStatus.emit(status);
}

private _transition($transition$: Transition) {
private processTransition($transition$: Transition) {
let sref = this.sref;

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

let srefTarget: TargetState = this._stateService.target(sref.state, sref.params, sref.options);
let srefTarget: TargetState = this._stateService.target(sref.state, sref.params, sref.getOptions());
if (!srefTarget.exists()) {
return this._setStatus(status);
}

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

status.active = tc.from.map(isTarget).reduce(anyTrueR, false);
status.exact = tail(tc.from.map(isTarget)) === true;
status.entering = tc.entering.map(isTarget).reduce(anyTrueR, false);
status.exiting = tc.exiting.map(isTarget).reduce(anyTrueR, false);
/**
* Returns a Predicate<Node[]> that returns true when the target state (and any param values)
* match the (tail of) the path, and the path's param values
*/
const pathMatches = (target: TargetState) => {
let state: State = target.$state();
let targetParamVals = target.params();
let targetPath: Node[] = PathFactory.buildPath(target);
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
.reduce(unnestR, [])
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));

return (path: Node[]) => {
let tailNode = tail(path);
if (!tailNode || tailNode.state !== state) return false;
var paramValues = PathFactory.paramValues(path);
return Param.equals(paramSchema, paramValues, targetParamVals);
};
};

const isTarget = pathMatches(srefTarget);

/**
* Given path: [c, d] appendTo: [a, b]),
* Expands the path to [c], [c, d]
* Then appends each to [a,b,] and returns: [a, b, c], [a, b, c, d]
*/
function spreadToSubPaths (path: Node[], appendTo: Node[] = []): Node[][] {
return path.map(node => appendTo.concat(PathFactory.subPath(path, node.state)));
}

let tc: TreeChanges = $transition$.treeChanges();
status.active = spreadToSubPaths(tc.from).map(isTarget).reduce(anyTrueR, false);
status.exact = isTarget(tc.from);
status.entering = spreadToSubPaths(tc.entering, tc.retained).map(isTarget).reduce(anyTrueR, false);
status.exiting = spreadToSubPaths(tc.exiting, tc.retained).map(isTarget).reduce(anyTrueR, false);

if ($transition$.isActive()) {
this._setStatus(status);
}

let update = (currentPath: Node[]) => () => {
if (this._deregisterHook == null) return; // destroyed
if (!$transition$.isActive()) return; // superseded
status.active = currentPath.map(isTarget).reduce(anyTrueR, false);
status.exact = tail(currentPath.map(isTarget)) === true;
status.active = spreadToSubPaths(currentPath).map(isTarget).reduce(anyTrueR, false);
status.exact = isTarget(currentPath);
status.entering = status.exiting = false;
this._setStatus(status);
};
Expand Down
16 changes: 12 additions & 4 deletions src/path/pathFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ export class PathFactory {
return new TargetState(state, state, path.map(prop("paramValues")).reduce(mergeR, {}));
}

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

if (targetState.options().inherit) toPath = PathFactory.inheritParams(fromPath, toPath, Object.keys(toParams));
/** Given a fromPath: Node[] and a TargetState, builds a toPath: Node[] */
static buildToPath(fromPath: Node[], targetState: TargetState): Node[] {
let toPath: Node[] = PathFactory.buildPath(targetState);
if (targetState.options().inherit) {
return PathFactory.inheritParams(fromPath, toPath, Object.keys(targetState.params()));
}
return toPath;
}

Expand Down Expand Up @@ -159,4 +164,7 @@ export class PathFactory {
if (elementIdx === -1) throw new Error("The path does not contain the state: " + state);
return path.slice(0, elementIdx + 1);
}

/** Gets the raw parameter values from a path */
static paramValues = (path: Node[]) => path.reduce((acc, node) => extend(acc, node.paramValues), {});
}

0 comments on commit 46cdf4c

Please sign in to comment.