Skip to content

Commit

Permalink
fix(ng2.uiSrefActive): Allow uiSrefActive on ancestor element.
Browse files Browse the repository at this point in the history
feat(ng2.uiSrefActive): Refactor using observables
Closes #2950
  • Loading branch information
christopherthielen committed Aug 31, 2016
1 parent 2a2f381 commit 874fc07
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 105 deletions.
36 changes: 29 additions & 7 deletions src/ng2/directives/uiSref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {Renderer} from "@angular/core";
import {UIView, ParentUIViewInject} from "./uiView";
import {extend, Obj} from "../../common/common";
import {TransitionOptions} from "../../transition/interface";
import {Globals, UIRouterGlobals} from "../../globals";
import {Subscription, ReplaySubject} from "rxjs/Rx";
import {TargetState} from "../../state/targetState";

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

public targetState$ = new ReplaySubject<TargetState>(1);
private _emit: boolean = false;

private _statesSub: Subscription;

constructor(
private _router: UIRouter,
@Inject(UIView.PARENT_INJECT) public parent: ParentUIViewInject,
@Optional() private _anchorUISref: AnchorUISref
) { }
@Optional() private _anchorUISref: AnchorUISref,
@Inject(Globals) _globals: UIRouterGlobals
) {
this._statesSub = _globals.states$.subscribe(() => this.update())
}

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

ngOnInit() {
this._emit = true;
this.update();
}

ngOnDestroy() {
this._statesSub.unsubscribe();
this.targetState$.unsubscribe();
}

update() {
let $state = this._router.stateService;
if (this._emit) {
let newTarget = $state.target(this.state, this.params, this.getOptions());
this.targetState$.next(newTarget);
}

if (this._anchorUISref) {
this._anchorUISref.update(this._router.stateService.href(this.state, this.params, this.getOptions()));
let href = $state.href(this.state, this.params, this.getOptions());
this._anchorUISref.update(href);
}
}

getOptions() {
let defOpts: TransitionOptions = {
let defaultOpts: TransitionOptions = {
relative: this.parent && this.parent.context && this.parent.context.name,
inherit: true ,
source: "sref"
};
return extend(defOpts, this.options || {});
return extend(defaultOpts, this.options || {});
}

go() {
this._router.stateService.go(this.state, this.params, this.getOptions());
return false;
}
}


8 changes: 7 additions & 1 deletion src/ng2/directives/uiSrefActive.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @module ng2_directives */ /** */
import {Directive, Input, ElementRef, Host, Renderer} from "@angular/core";
import {UISrefStatus, SrefStatus} from "./uiSrefStatus";
import {Subscription} from "rxjs/Rx";

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

private _subscription: Subscription;
constructor(uiSrefStatus: UISrefStatus, rnd: Renderer, @Host() host: ElementRef) {
uiSrefStatus.uiSrefStatus.subscribe((next: SrefStatus) => {
this._subscription = uiSrefStatus.uiSrefStatus.subscribe((next: SrefStatus) => {
this._classes.forEach(cls => rnd.setElementClass(host.nativeElement, cls, next.active));
this._classesEq.forEach(cls => rnd.setElementClass(host.nativeElement, cls, next.exact));
});
}

ngOnDestroy() {
this._subscription.unsubscribe();
}
}
223 changes: 126 additions & 97 deletions src/ng2/directives/uiSrefStatus.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/** @module ng2_directives */ /** */
import {Directive, Output, EventEmitter, ContentChild} from "@angular/core";
import {StateService} from "../../state/stateService";
import {Directive, Output, EventEmitter, ContentChildren, QueryList, Inject} from "@angular/core";
import {UISref} from "./uiSref";
import {PathNode} 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, unnestR} from "../../common/common";
import {Globals} from "../../globals";
import {anyTrueR, tail, unnestR, Predicate} from "../../common/common";
import {Globals, UIRouterGlobals} from "../../globals";
import {Param} from "../../params/param";
import {PathFactory} from "../../path/pathFactory";
import {Subscription, Observable} from "rxjs/Rx";

interface TransEvt { evt: string, trans: Transition }

/**
* uiSref status booleans
Expand All @@ -27,6 +27,84 @@ export interface SrefStatus {
exiting: boolean;
}

const inactiveStatus: SrefStatus = {
active: false,
exact: false,
entering: false,
exiting: false
};

/**
* Returns a Predicate<PathNode[]>
*
* The predicate returns true when the target state (and param values)
* match the (tail of) the path, and the path's param values
*/
const pathMatches = (target: TargetState): Predicate<PathNode[]> => {
let state: State = target.$state();
let targetParamVals = target.params();
let targetPath: PathNode[] = PathFactory.buildPath(target);
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
.reduce(unnestR, [])
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));

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

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

/**
* Given a TransEvt (Transition event: started, success, error)
* and a UISref Target State, return a SrefStatus object
* which represents the current status of that Sref:
* active, activeEq (exact match), entering, exiting
*/
function getSrefStatus(event: TransEvt, srefTarget: TargetState): SrefStatus {
const pathMatchesTarget = pathMatches(srefTarget);
const tc = event.trans.treeChanges();

let isStartEvent = event.evt === 'start';
let isSuccessEvent = event.evt === 'success';
let activePath: PathNode[] = isSuccessEvent ? tc.to : tc.from;

const isActive = () =>
spreadToSubPaths([], activePath)
.map(pathMatchesTarget)
.reduce(anyTrueR, false);

const isExact = () =>
pathMatchesTarget(activePath);

const isEntering = () =>
spreadToSubPaths(tc.retained, tc.entering)
.map(pathMatchesTarget)
.reduce(anyTrueR, false);

const isExiting = () =>
spreadToSubPaths(tc.retained, tc.exiting)
.map(pathMatchesTarget)
.reduce(anyTrueR, false);

return {
active: isActive(),
exact: isExact(),
entering: isStartEvent ? isEntering() : false,
exiting: isStartEvent ? isExiting() : false,
} as SrefStatus;
}

/**
* A directive (which pairs with a [[UISref]]) and emits events when the UISref status changes.
*
Expand All @@ -40,110 +118,61 @@ export interface SrefStatus {
*/
@Directive({ selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]' })
export class UISrefStatus {
private _deregisterHook: Function;

// current statuses of the state/params the uiSref directive is linking to
/** current statuses of the state/params the uiSref directive is linking to */
@Output("uiSrefStatus") uiSrefStatus = new EventEmitter<SrefStatus>(false);
@ContentChild(UISref) sref: UISref;
/** Monitor all child components for UISref(s) */
@ContentChildren(UISref, {descendants: true}) srefs: QueryList<UISref>;

status: SrefStatus = {
active: false,
exact: false,
entering: false,
exiting: false
};
/** The current status */
status: SrefStatus;

private _subscription: Subscription;

constructor(transitionService: TransitionService,
private _globals: Globals,
private _stateService: StateService) {
this._deregisterHook = transitionService.onStart({}, $transition$ => this.processTransition($transition$));
constructor(@Inject(Globals) private _globals: UIRouterGlobals) {
this.status = Object.assign({}, inactiveStatus);
}

ngAfterContentInit() {
let lastTrans = this._globals.transitionHistory.peekTail();
if (lastTrans != null) {
this.processTransition(lastTrans);
}
// Map each transition start event to a stream of:
// start -> (success|error)
let transEvents$: Observable<TransEvt> = this._globals.start$.switchMap((trans: Transition) => {
const event = (evt: string) => ({evt, trans} as TransEvt);

let transStart$ = Observable.of(event("start"));
let transResult = trans.promise.then(() => event("success"), () => event("error"));
let transFinish$ = Observable.fromPromise(transResult);

return transStart$.concat(transFinish$);
});

// Watch the children UISref components and get their target states
let srefs$: Observable<UISref[]> = Observable.of(this.srefs.toArray()).concat(this.srefs.changes);
let targetStates$: Observable<TargetState[]> =
srefs$.switchMap((srefs: UISref[]) =>
Observable.combineLatest<TargetState[]>(srefs.map(sref => sref.targetState$)));

// Calculate the status of each UISref based on the transition event.
// Reduce the statuses (if multiple) by or-ing each flag.
this._subscription = transEvents$.mergeMap((evt: TransEvt) => {
return targetStates$.map((targets: TargetState[]) => {
let statuses: SrefStatus[] = targets.map(target => getSrefStatus(evt, target));

return statuses.reduce((acc: SrefStatus, val: SrefStatus) => ({
active: acc.active || val.active,
exact: acc.active || val.active,
entering: acc.active || val.active,
exiting: acc.active || val.active,
}))
})
}).subscribe(this._setStatus.bind(this));
}

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

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

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

let status: SrefStatus = <any> {
active: false,
exact: false,
entering: false,
exiting: false
};

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


/**
* Returns a Predicate<PathNode[]> 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: PathNode[] = PathFactory.buildPath(target);
let paramSchema: Param[] = targetPath.map(node => node.paramSchema)
.reduce(unnestR, [])
.filter((param: Param) => targetParamVals.hasOwnProperty(param.id));

return (path: PathNode[]) => {
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: PathNode[], appendTo: PathNode[] = []): PathNode[][] {
return path.map(node => appendTo.concat(PathFactory.subPath(path, n => n.state === 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: PathNode[]) => () => {
if (this._deregisterHook == null) return; // destroyed
if (!$transition$.isActive()) return; // superseded
status.active = spreadToSubPaths(currentPath).map(isTarget).reduce(anyTrueR, false);
status.exact = isTarget(currentPath);
status.entering = status.exiting = false;
this._setStatus(status);
};

$transition$.promise.then(update(tc.to), update(tc.from));
}
}

0 comments on commit 874fc07

Please sign in to comment.