Skip to content

Commit

Permalink
fix(ng2.uiSrefActive): Allow ng-if on nested uiSrefs
Browse files Browse the repository at this point in the history
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
christopherthielen committed Oct 2, 2016
1 parent b00f044 commit e3051f5
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 27 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/ng2/directives/uiSref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export class UISref {
}

ngOnDestroy() {
this._emit = false;
this._statesSub.unsubscribe();
this.targetState$.unsubscribe();
}
Expand Down
73 changes: 65 additions & 8 deletions src/ng2/directives/uiSrefActive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down
77 changes: 60 additions & 17 deletions src/ng2/directives/uiSrefStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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.
*/
Expand All @@ -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);
Expand All @@ -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) {
Expand Down

0 comments on commit e3051f5

Please sign in to comment.