Skip to content

Commit e3051f5

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
1 parent b00f044 commit e3051f5

File tree

4 files changed

+128
-27
lines changed

4 files changed

+128
-27
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test:ng13": "karma start config/karma.ng13.js",
1616
"test:ng14": "karma start config/karma.ng14.js",
1717
"test:ng15": "karma start config/karma.ng15.js",
18+
"test:ng2": "karma start config/karma.ng2.js",
1819
"test:integrate": "tsc && npm run test:core && npm run test:ng12 && npm run test:ng13 && npm run test:ng14 && npm run test:ng15",
1920
"docs": "./scripts/docs.sh"
2021
},
@@ -80,12 +81,11 @@
8081
"karma-chrome-launcher": "~0.1.0",
8182
"karma-coverage": "^0.5.3",
8283
"karma-jasmine": "^1.0.2",
83-
"karma-phantomjs-launcher": "~0.1.0",
84+
"karma-phantomjs-launcher": "^1.0.2",
8485
"karma-script-launcher": "~0.1.0",
8586
"karma-systemjs": "^0.7.2",
8687
"lodash": "^4.5.1",
8788
"parallelshell": "^2.0.0",
88-
"phantomjs-polyfill": "0.0.1",
8989
"remap-istanbul": "^0.6.3",
9090
"rxjs": "5.0.0-beta.12",
9191
"shelljs": "^0.7.0",

src/ng2/directives/uiSref.ts

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export class UISref {
9494
}
9595

9696
ngOnDestroy() {
97+
this._emit = false;
9798
this._statesSub.unsubscribe();
9899
this.targetState$.unsubscribe();
99100
}

src/ng2/directives/uiSrefActive.ts

+65-8
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,86 @@ import {UISrefStatus, SrefStatus} from "./uiSrefStatus";
44
import {Subscription} from "rxjs/Rx";
55

66
/**
7-
* A directive that adds a CSS class when a `uiSref` is active.
7+
* A directive that adds a CSS class when its associated `uiSref` link is active.
88
*
99
* ### Purpose
1010
*
11-
* This directive should be paired with a [[UISref]], and is used to apply a CSS class to the element when
12-
* the state that the `uiSref` targets is active.
11+
* This directive should be paired with one (or more) [[UISref]] directives.
12+
* It will apply a CSS class to its element when the state the `uiSref` targets is activated.
13+
*
14+
* This can be used to create navigation UI where the active link is highlighted.
1315
*
1416
* ### Selectors
1517
*
1618
* - `[uiSrefActive]`: When this selector is used, the class is added when the target state or any
1719
* child of the target state is active
18-
* - `[uiSrefActiveEq]`: When this selector is used, the class is added when the target state is directly active
20+
* - `[uiSrefActiveEq]`: When this selector is used, the class is added when the target state is
21+
* exactly active (the class is not added if a child of the target state is active).
1922
*
2023
* ### Inputs
2124
*
22-
* - `uiSrefActive`/`uiSrefActiveEq`: one or more CSS classes to add to the element, when active
25+
* - `uiSrefActive`/`uiSrefActiveEq`: one or more CSS classes to add to the element, when the `uiSref` is active
2326
*
24-
* @example
27+
* #### Example:
28+
* The anchor tag has the `active` class added when the `foo` state is active.
2529
* ```html
26-
*
2730
* <a uiSref="foo" uiSrefActive="active">Foo</a>
28-
* <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Foo Bar #{{bar.id}}</a>
2931
* ```
32+
*
33+
* ### Matching parameters
34+
*
35+
* If the `uiSref` includes parameters, the current state must be active, *and* the parameter values must match.
36+
*
37+
* #### Example:
38+
* The first anchor tag has the `active` class added when the `foo.bar` state is active and the `id` parameter
39+
* equals 25.
40+
* The second anchor tag has the `active` class added when the `foo.bar` state is active and the `id` parameter
41+
* equals 32.
42+
* ```html
43+
* <a uiSref="foo.bar" [uiParams]="{ id: 25 }" uiSrefActive="active">Bar #25</a>
44+
* <a uiSref="foo.bar" [uiParams]="{ id: 32 }" uiSrefActive="active">Bar #32</a>
45+
* ```
46+
*
47+
* #### Example:
48+
* A list of anchor tags are created for a list of `bar` objects.
49+
* An anchor tag will have the `active` class when `foo.bar` state is active and the `id` parameter matches
50+
* that object's `id`.
51+
* ```html
52+
* <li *ngFor="let bar of bars">
53+
* <a uiSref="foo.bar" [uiParams]="{ id: bar.id }" uiSrefActive="active">Bar #{{ bar.id }}</a>
54+
* </li>
55+
* ```
56+
*
57+
* ### Multiple uiSrefs
58+
*
59+
* A single `uiSrefActive` can be used for multiple `uiSref` links.
60+
* This can be used to create (for example) a drop down navigation menu, where the menui is highlighted
61+
* if *any* of its inner links are active.
62+
*
63+
* The `uiSrefActive` should be placed on an ancestor element of the `uiSref` list.
64+
* If anyof the `uiSref` links are activated, the class will be added to the ancestor element.
65+
*
66+
* #### Example:
67+
* This is a dropdown nagivation menu for "Admin" states.
68+
* When any of `admin.users`, `admin.groups`, `admin.settings` are active, the `<li>` for the dropdown
69+
* has the `dropdown-child-active` class applied.
70+
* Additionally, the active anchor tag has the `active` class applied.
71+
* ```html
72+
* <ul class="dropdown-menu">
73+
* <li uiSrefActive="dropdown-child-active" class="dropdown admin">
74+
* Admin
75+
* <ul>
76+
* <li><a uiSref="admin.users" uiSrefActive="active">Users</a></li>
77+
* <li><a uiSref="admin.groups" uiSrefActive="active">Groups</a></li>
78+
* <li><a uiSref="admin.settings" uiSrefActive="active">Settings</a></li>
79+
* </ul>
80+
* </li>
81+
* </ul>
82+
* ```
83+
*
84+
* ---
85+
*
86+
* As
3087
*/
3188
@Directive({
3289
selector: '[uiSrefActive],[uiSrefActiveEq]'

src/ng2/directives/uiSrefStatus.ts

+60-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {anyTrueR, tail, unnestR, Predicate} from "../../common/common";
99
import {Globals, UIRouterGlobals} from "../../globals";
1010
import {Param} from "../../params/param";
1111
import {PathFactory} from "../../path/pathFactory";
12-
import {Subscription, Observable} from "rxjs/Rx";
12+
import {Subscription, Observable, BehaviorSubject} from "rxjs/Rx";
1313

1414
interface TransEvt { evt: string, trans: Transition }
1515

@@ -106,14 +106,54 @@ function getSrefStatus(event: TransEvt, srefTarget: TargetState): SrefStatus {
106106
} as SrefStatus;
107107
}
108108

109+
function mergeSrefStatus(left: SrefStatus, right: SrefStatus) {
110+
return {
111+
active: left.active || right.active,
112+
exact: left.exact || right.exact,
113+
entering: left.entering || right.entering,
114+
exiting: left.exiting || right.exiting,
115+
};
116+
}
117+
109118
/**
110-
* A directive (which pairs with a [[UISref]]) and emits events when the UISref status changes.
119+
* A directive which emits events when a paired [[UISref]] status changes.
120+
*
121+
* This directive is primarily used by the [[UISrefActive]]/[[UISrefActiveEq]] directives to monitor `UISref`(s).
122+
* This directive shares the same attribute selectors as `UISrefActive/Eq`, so it is created whenever a `UISrefActive/Eq` is created.
123+
*
124+
* Most apps should simply use [[UISrefActive]], but some advanced components may want to process the
125+
* `uiSrefStatus` events directly.
126+
*
127+
* ```js
128+
* <li (uiSrefStatus)="onSrefStatusChanged($event)">
129+
* <a uiSref="book" [uiParams]="{ bookId: book.id }">Book {{ book.name }}</a>
130+
* </li>
131+
* ```
132+
*
133+
* The `uiSrefStatus` event is emitted whenever an enclosed `uiSref`'s status changes.
134+
* The event emitted is of type [[SrefStatus]], and has boolean values for `active`, `exact`, `entering`, and `exiting`.
111135
*
112-
* This directive is used by the [[UISrefActive]] directive.
113-
*
114-
* The event emitted is of type [[SrefStatus]], and has boolean values for `active`, `exact`, `entering`, and `exiting`
115-
*
116-
* The values from this event can be captured and stored on a component, then applied (perhaps using ngClass).
136+
* The values from this event can be captured and stored on a component (then applied, e.g., using ngClass).
137+
*
138+
* ---
139+
*
140+
* A single `uiSrefStatus` can enclose multiple `uiSref`.
141+
* Each status boolean (`active`, `exact`, `entering`, `exiting`) will be true if *any of the enclosed `uiSref` status is true*.
142+
* In other words, all enclosed `uiSref` statuses are merged to a single status using `||` (logical or).
143+
*
144+
* ```js
145+
* <li (uiSrefStatus)="onSrefStatus($event)" uiSref="admin">
146+
* Home
147+
* <ul>
148+
* <li> <a uiSref="admin.users">Users</a> </li>
149+
* <li> <a uiSref="admin.groups">Groups</a> </li>
150+
* </ul>
151+
* </li>
152+
* ```
153+
*
154+
* In the above example, `$event.active === true` when either `admin.users` or `admin.groups` is active.
155+
*
156+
* ---
117157
*
118158
* This API is subject to change.
119159
*/
@@ -128,6 +168,8 @@ export class UISrefStatus {
128168
status: SrefStatus;
129169

130170
private _subscription: Subscription;
171+
private _srefChangesSub: Subscription;
172+
private _srefs$: BehaviorSubject<UISref[]>;
131173

132174
constructor(@Inject(Globals) private _globals: UIRouterGlobals) {
133175
this.status = Object.assign({}, inactiveStatus);
@@ -146,30 +188,31 @@ export class UISrefStatus {
146188
return transStart$.concat(transFinish$);
147189
});
148190

149-
// Watch the children UISref components and get their target states
150-
let srefs$: Observable<UISref[]> = Observable.of(this.srefs.toArray()).concat(this.srefs.changes);
191+
// Watch the @ContentChildren UISref[] components and get their target states
192+
193+
// let srefs$: Observable<UISref[]> = Observable.of(this.srefs.toArray()).concat(this.srefs.changes);
194+
this._srefs$ = new BehaviorSubject(this.srefs.toArray());
195+
this._srefChangesSub = this.srefs.changes.subscribe(srefs => this._srefs$.next(srefs));
196+
151197
let targetStates$: Observable<TargetState[]> =
152-
srefs$.switchMap((srefs: UISref[]) =>
198+
this._srefs$.switchMap((srefs: UISref[]) =>
153199
Observable.combineLatest<TargetState[]>(srefs.map(sref => sref.targetState$)));
154200

155201
// Calculate the status of each UISref based on the transition event.
156202
// Reduce the statuses (if multiple) by or-ing each flag.
157203
this._subscription = transEvents$.mergeMap((evt: TransEvt) => {
158204
return targetStates$.map((targets: TargetState[]) => {
159205
let statuses: SrefStatus[] = targets.map(target => getSrefStatus(evt, target));
160-
161-
return statuses.reduce((acc: SrefStatus, val: SrefStatus) => ({
162-
active: acc.active || val.active,
163-
exact: acc.active || val.active,
164-
entering: acc.active || val.active,
165-
exiting: acc.active || val.active,
166-
}))
206+
return statuses.reduce(mergeSrefStatus)
167207
})
168208
}).subscribe(this._setStatus.bind(this));
169209
}
170210

171211
ngOnDestroy() {
172212
if (this._subscription) this._subscription.unsubscribe();
213+
if (this._srefChangesSub) this._srefChangesSub.unsubscribe();
214+
if (this._srefs$) this._srefs$.unsubscribe();
215+
this._subscription = this._srefChangesSub = this._srefs$ = undefined;
173216
}
174217

175218
private _setStatus(status: SrefStatus) {

0 commit comments

Comments
 (0)