From e3051f58013a5039a9419764f68db6c143c21a86 Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Sun, 2 Oct 2016 11:37:27 -0500 Subject: [PATCH] fix(ng2.uiSrefActive): Allow ng-if on nested uiSrefs When a `uiSrefActive` wraps multiple `uiSref` directives: when some uiSref were removed from the dom (via `*ngIf`), the observables weren't resetting properly. This change uses a BehaviorSubject to monitor the initial uiSref[] and any changes to the list. Closes #3046 --- package.json | 4 +- src/ng2/directives/uiSref.ts | 1 + src/ng2/directives/uiSrefActive.ts | 73 ++++++++++++++++++++++++---- src/ng2/directives/uiSrefStatus.ts | 77 +++++++++++++++++++++++------- 4 files changed, 128 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 8e54f6abd..a89d88713 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:ng13": "karma start config/karma.ng13.js", "test:ng14": "karma start config/karma.ng14.js", "test:ng15": "karma start config/karma.ng15.js", + "test:ng2": "karma start config/karma.ng2.js", "test:integrate": "tsc && npm run test:core && npm run test:ng12 && npm run test:ng13 && npm run test:ng14 && npm run test:ng15", "docs": "./scripts/docs.sh" }, @@ -80,12 +81,11 @@ "karma-chrome-launcher": "~0.1.0", "karma-coverage": "^0.5.3", "karma-jasmine": "^1.0.2", - "karma-phantomjs-launcher": "~0.1.0", + "karma-phantomjs-launcher": "^1.0.2", "karma-script-launcher": "~0.1.0", "karma-systemjs": "^0.7.2", "lodash": "^4.5.1", "parallelshell": "^2.0.0", - "phantomjs-polyfill": "0.0.1", "remap-istanbul": "^0.6.3", "rxjs": "5.0.0-beta.12", "shelljs": "^0.7.0", diff --git a/src/ng2/directives/uiSref.ts b/src/ng2/directives/uiSref.ts index 81ceb0907..cf4e6cb18 100644 --- a/src/ng2/directives/uiSref.ts +++ b/src/ng2/directives/uiSref.ts @@ -94,6 +94,7 @@ export class UISref { } ngOnDestroy() { + this._emit = false; this._statesSub.unsubscribe(); this.targetState$.unsubscribe(); } diff --git a/src/ng2/directives/uiSrefActive.ts b/src/ng2/directives/uiSrefActive.ts index 193d9a04b..91c0101a4 100644 --- a/src/ng2/directives/uiSrefActive.ts +++ b/src/ng2/directives/uiSrefActive.ts @@ -4,29 +4,86 @@ import {UISrefStatus, SrefStatus} from "./uiSrefStatus"; import {Subscription} from "rxjs/Rx"; /** - * A directive that adds a CSS class when a `uiSref` is active. + * A directive that adds a CSS class when its associated `uiSref` link is active. * * ### Purpose * - * This directive should be paired with a [[UISref]], and is used to apply a CSS class to the element when - * the state that the `uiSref` targets is active. + * This directive should be paired with one (or more) [[UISref]] directives. + * It will apply a CSS class to its element when the state the `uiSref` targets is activated. + * + * This can be used to create navigation UI where the active link is highlighted. * * ### Selectors * * - `[uiSrefActive]`: When this selector is used, the class is added when the target state or any * child of the target state is active - * - `[uiSrefActiveEq]`: When this selector is used, the class is added when the target state is directly active + * - `[uiSrefActiveEq]`: When this selector is used, the class is added when the target state is + * exactly active (the class is not added if a child of the target state is active). * * ### Inputs * - * - `uiSrefActive`/`uiSrefActiveEq`: one or more CSS classes to add to the element, when active + * - `uiSrefActive`/`uiSrefActiveEq`: one or more CSS classes to add to the element, when the `uiSref` is active * - * @example + * #### Example: + * The anchor tag has the `active` class added when the `foo` state is active. * ```html - * * Foo - * Foo Bar #{{bar.id}} * ``` + * + * ### Matching parameters + * + * If the `uiSref` includes parameters, the current state must be active, *and* the parameter values must match. + * + * #### Example: + * The first anchor tag has the `active` class added when the `foo.bar` state is active and the `id` parameter + * equals 25. + * The second anchor tag has the `active` class added when the `foo.bar` state is active and the `id` parameter + * equals 32. + * ```html + * Bar #25 + * Bar #32 + * ``` + * + * #### Example: + * A list of anchor tags are created for a list of `bar` objects. + * An anchor tag will have the `active` class when `foo.bar` state is active and the `id` parameter matches + * that object's `id`. + * ```html + *
  • + * Bar #{{ bar.id }} + *
  • + * ``` + * + * ### Multiple uiSrefs + * + * A single `uiSrefActive` can be used for multiple `uiSref` links. + * This can be used to create (for example) a drop down navigation menu, where the menui is highlighted + * if *any* of its inner links are active. + * + * The `uiSrefActive` should be placed on an ancestor element of the `uiSref` list. + * If anyof the `uiSref` links are activated, the class will be added to the ancestor element. + * + * #### Example: + * This is a dropdown nagivation menu for "Admin" states. + * When any of `admin.users`, `admin.groups`, `admin.settings` are active, the `
  • ` for the dropdown + * has the `dropdown-child-active` class applied. + * Additionally, the active anchor tag has the `active` class applied. + * ```html + * + * ``` + * + * --- + * + * As */ @Directive({ selector: '[uiSrefActive],[uiSrefActiveEq]' diff --git a/src/ng2/directives/uiSrefStatus.ts b/src/ng2/directives/uiSrefStatus.ts index 94d2bd90e..f230f09b8 100644 --- a/src/ng2/directives/uiSrefStatus.ts +++ b/src/ng2/directives/uiSrefStatus.ts @@ -9,7 +9,7 @@ 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"; +import {Subscription, Observable, BehaviorSubject} from "rxjs/Rx"; interface TransEvt { evt: string, trans: Transition } @@ -106,14 +106,54 @@ function getSrefStatus(event: TransEvt, srefTarget: TargetState): SrefStatus { } as SrefStatus; } +function mergeSrefStatus(left: SrefStatus, right: SrefStatus) { + return { + active: left.active || right.active, + exact: left.exact || right.exact, + entering: left.entering || right.entering, + exiting: left.exiting || right.exiting, + }; +} + /** - * A directive (which pairs with a [[UISref]]) and emits events when the UISref status changes. + * A directive which emits events when a paired [[UISref]] status changes. + * + * This directive is primarily used by the [[UISrefActive]]/[[UISrefActiveEq]] directives to monitor `UISref`(s). + * This directive shares the same attribute selectors as `UISrefActive/Eq`, so it is created whenever a `UISrefActive/Eq` is created. + * + * Most apps should simply use [[UISrefActive]], but some advanced components may want to process the + * `uiSrefStatus` events directly. + * + * ```js + *
  • + * Book {{ book.name }} + *
  • + * ``` + * + * The `uiSrefStatus` event is emitted whenever an enclosed `uiSref`'s status changes. + * The event emitted is of type [[SrefStatus]], and has boolean values for `active`, `exact`, `entering`, and `exiting`. * - * This directive is used by the [[UISrefActive]] directive. - * - * 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). + * The values from this event can be captured and stored on a component (then applied, e.g., using ngClass). + * + * --- + * + * A single `uiSrefStatus` can enclose multiple `uiSref`. + * Each status boolean (`active`, `exact`, `entering`, `exiting`) will be true if *any of the enclosed `uiSref` status is true*. + * In other words, all enclosed `uiSref` statuses are merged to a single status using `||` (logical or). + * + * ```js + *
  • + * Home + * + *
  • + * ``` + * + * In the above example, `$event.active === true` when either `admin.users` or `admin.groups` is active. + * + * --- * * This API is subject to change. */ @@ -128,6 +168,8 @@ export class UISrefStatus { status: SrefStatus; private _subscription: Subscription; + private _srefChangesSub: Subscription; + private _srefs$: BehaviorSubject; constructor(@Inject(Globals) private _globals: UIRouterGlobals) { this.status = Object.assign({}, inactiveStatus); @@ -146,10 +188,14 @@ export class UISrefStatus { return transStart$.concat(transFinish$); }); - // Watch the children UISref components and get their target states - let srefs$: Observable = Observable.of(this.srefs.toArray()).concat(this.srefs.changes); + // Watch the @ContentChildren UISref[] components and get their target states + + // let srefs$: Observable = Observable.of(this.srefs.toArray()).concat(this.srefs.changes); + this._srefs$ = new BehaviorSubject(this.srefs.toArray()); + this._srefChangesSub = this.srefs.changes.subscribe(srefs => this._srefs$.next(srefs)); + let targetStates$: Observable = - srefs$.switchMap((srefs: UISref[]) => + this._srefs$.switchMap((srefs: UISref[]) => Observable.combineLatest(srefs.map(sref => sref.targetState$))); // Calculate the status of each UISref based on the transition event. @@ -157,19 +203,16 @@ export class UISrefStatus { 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, - })) + return statuses.reduce(mergeSrefStatus) }) }).subscribe(this._setStatus.bind(this)); } ngOnDestroy() { if (this._subscription) this._subscription.unsubscribe(); + if (this._srefChangesSub) this._srefChangesSub.unsubscribe(); + if (this._srefs$) this._srefs$.unsubscribe(); + this._subscription = this._srefChangesSub = this._srefs$ = undefined; } private _setStatus(status: SrefStatus) {