Permalink
Browse files

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
  • Loading branch information...
1 parent b00f044 commit e3051f58013a5039a9419764f68db6c143c21a86 @christopherthielen christopherthielen committed Oct 2, 2016
Showing with 128 additions and 27 deletions.
  1. +2 −2 package.json
  2. +1 −0 src/ng2/directives/uiSref.ts
  3. +65 −8 src/ng2/directives/uiSrefActive.ts
  4. +60 −17 src/ng2/directives/uiSrefStatus.ts
View
@@ -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",
@@ -94,6 +94,7 @@ export class UISref {
}
ngOnDestroy() {
+ this._emit = false;
this._statesSub.unsubscribe();
this.targetState$.unsubscribe();
}
@@ -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
- *
* <a uiSref="foo" uiSrefActive="active">Foo</a>
- * <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Foo Bar #{{bar.id}}</a>
* ```
+ *
+ * ### 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
+ * <a uiSref="foo.bar" [uiParams]="{ id: 25 }" uiSrefActive="active">Bar #25</a>
+ * <a uiSref="foo.bar" [uiParams]="{ id: 32 }" uiSrefActive="active">Bar #32</a>
+ * ```
+ *
+ * #### 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
+ * <li *ngFor="let bar of bars">
+ * <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Bar #{{ bar.id }}</a>
+ * </li>
+ * ```
+ *
+ * ### 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 `<li>` for the dropdown
+ * has the `dropdown-child-active` class applied.
+ * Additionally, the active anchor tag has the `active` class applied.
+ * ```html
+ * <ul class="dropdown-menu">
+ * <li uiSrefActive="dropdown-child-active" class="dropdown admin">
+ * Admin
+ * <ul>
+ * <li><a uiSref="admin.users" uiSrefActive="active">Users</a></li>
+ * <li><a uiSref="admin.groups" uiSrefActive="active">Groups</a></li>
+ * <li><a uiSref="admin.settings" uiSrefActive="active">Settings</a></li>
+ * </ul>
+ * </li>
+ * </ul>
+ * ```
+ *
+ * ---
+ *
+ * As
*/
@Directive({
selector: '[uiSrefActive],[uiSrefActiveEq]'
@@ -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
+ * <li (uiSrefStatus)="onSrefStatusChanged($event)">
+ * <a uiSref="book" [uiParams]="{ bookId: book.id }">Book {{ book.name }}</a>
+ * </li>
+ * ```
+ *
+ * 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
+ * <li (uiSrefStatus)="onSrefStatus($event)" uiSref="admin">
+ * Home
+ * <ul>
+ * <li> <a uiSref="admin.users">Users</a> </li>
+ * <li> <a uiSref="admin.groups">Groups</a> </li>
+ * </ul>
+ * </li>
+ * ```
+ *
+ * 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<UISref[]>;
constructor(@Inject(Globals) private _globals: UIRouterGlobals) {
this.status = Object.assign({}, inactiveStatus);
@@ -146,30 +188,31 @@ export class UISrefStatus {
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);
+ // Watch the @ContentChildren UISref[] components and get their target states
+
+ // let srefs$: Observable<UISref[]> = 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<TargetState[]> =
- srefs$.switchMap((srefs: UISref[]) =>
+ this._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,
- }))
+ 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) {

0 comments on commit e3051f5

Please sign in to comment.