diff --git a/src/common/common.ts b/src/common/common.ts index 21a2c8ce6..d90d71f8c 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -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(fn: Predicate, errMsg: (string|Function) = "assert failure"): Predicate { +export function assertPredicate(predicate: Predicate, errMsg: (string|Function) = "assert failure"): Predicate { return (obj: T) => { - if (!fn(obj)) throw new Error(isFunction(errMsg) ? ( errMsg)(obj) : errMsg); + if (!predicate(obj)) { + throw new Error(isFunction(errMsg) ? ( errMsg)(obj) : errMsg); + } return true; }; } diff --git a/src/ng2/uiSref.ts b/src/ng2/uiSref.ts index d5edfc387..b0483a019 100644 --- a/src/ng2/uiSref.ts +++ b/src/ng2/uiSref.ts @@ -1,6 +1,6 @@ /** @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"; @@ -8,6 +8,7 @@ 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) { } @@ -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 + * + * + * Bar + * + * + * Foo Child + * + * + * Bar {{foo.barId}} + * ``` + */ @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, diff --git a/src/ng2/uiSrefActive.ts b/src/ng2/uiSrefActive.ts index baecc119a..117cfef94 100644 --- a/src/ng2/uiSrefActive.ts +++ b/src/ng2/uiSrefActive.ts @@ -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 + * + * Foo + * Foo Bar #{{bar.id}} + * ``` + */ +@Directive({ + selector: '[uiSrefActive],[uiSrefActiveEq]' +}) export class UiSrefActive { + private _classes: string[] = []; @Input('uiSrefActive') set active(val) { this._classes = val.split("\s+")}; @@ -17,4 +34,3 @@ export class UiSrefActive { }); } } - diff --git a/src/ng2/uiSrefStatus.ts b/src/ng2/uiSrefStatus.ts index 2f2b3d92f..1340c63dc 100644 --- a/src/ng2/uiSrefStatus.ts +++ b/src/ng2/uiSrefStatus.ts @@ -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. */ @@ -41,18 +53,21 @@ 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) { @@ -60,7 +75,7 @@ export class UiSrefStatus { this.uiSrefStatus.emit(status); } - private _transition($transition$: Transition) { + private processTransition($transition$: Transition) { let sref = this.sref; let status: SrefStatus = { @@ -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 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); }; diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 86688ee72..eb3d64242 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -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; } @@ -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), {}); }