Skip to content

Commit d4bc65d

Browse files
refactor: create internal state and reduce RxJS behavior subjects
Closes #5
1 parent 00564b4 commit d4bc65d

File tree

3 files changed

+126
-79
lines changed

3 files changed

+126
-79
lines changed

libs/angular-routing/src/lib/route.component.ts

+60-36
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import {
1414
OnDestroy,
1515
} from '@angular/core';
1616

17-
import { Subject, BehaviorSubject, merge, of, from } from 'rxjs';
17+
import { Subject, BehaviorSubject, of, from } from 'rxjs';
1818
import {
1919
distinctUntilChanged,
2020
filter,
2121
takeUntil,
2222
mergeMap,
2323
withLatestFrom,
24+
map,
2425
} from 'rxjs/operators';
2526

2627
import { Load, Route, RouteOptions } from './route';
@@ -36,6 +37,12 @@ export function getRoutePath(routeComponent: RouteComponent) {
3637
return routeComponent.routePath$;
3738
}
3839

40+
interface State {
41+
params: Params;
42+
path: string;
43+
shouldRender: boolean;
44+
}
45+
3946
@Component({
4047
// tslint:disable-next-line:component-selector
4148
selector: 'route',
@@ -81,13 +88,23 @@ export class RouteComponent implements OnInit, OnDestroy {
8188

8289
private _path: string;
8390
private destroy$ = new Subject();
84-
private _routeParams$ = new BehaviorSubject<Params>({});
85-
private _routePath$ = new BehaviorSubject<string>('');
86-
private _shouldRender$ = new BehaviorSubject<boolean>(false);
87-
88-
readonly shouldRender$ = this._shouldRender$.asObservable();
89-
readonly routeParams$ = this._routeParams$.pipe(takeUntil(this.destroy$));
90-
readonly routePath$ = this._routePath$.pipe(takeUntil(this.destroy$));
91+
private readonly state$ = new BehaviorSubject<State>({
92+
params: {},
93+
path: '',
94+
shouldRender: false,
95+
});
96+
97+
readonly shouldRender$ = this.state$.pipe(map((state) => state.shouldRender));
98+
readonly routeParams$ = this.state$.pipe(
99+
map((state) => state.params),
100+
distinctUntilChanged(),
101+
takeUntil(this.destroy$)
102+
);
103+
readonly routePath$ = this.state$.pipe(
104+
map((state) => state.path),
105+
distinctUntilChanged(),
106+
takeUntil(this.destroy$)
107+
);
91108
route!: Route;
92109

93110
constructor(
@@ -106,38 +123,41 @@ export class RouteComponent implements OnInit, OnDestroy {
106123

107124
this.route = this.registerRoute(path, this.exact, this.load);
108125

109-
const activeRoute$ = this.routerComponent.activeRoute$.pipe(
110-
filter((ar) => ar !== null),
111-
distinctUntilChanged(),
112-
withLatestFrom(this.shouldRender$),
113-
mergeMap(([current, rendered]) => {
114-
if (current.route === this.route) {
115-
this._routeParams$.next(current.params);
116-
this._routePath$.next(current.path);
117-
118-
if (this.redirectTo) {
119-
this.router.go(this.redirectTo);
120-
return of(null);
121-
}
126+
this.routerComponent.activeRoute$
127+
.pipe(
128+
filter((ar) => ar !== null),
129+
distinctUntilChanged(),
130+
withLatestFrom(this.shouldRender$),
131+
mergeMap(([current, rendered]) => {
132+
if (current.route === this.route) {
133+
if (this.redirectTo) {
134+
this.router.go(this.redirectTo);
135+
return of(null);
136+
}
137+
138+
this.updateState({
139+
params: current.params,
140+
path: current.path,
141+
});
122142

123-
if (!rendered) {
124-
if (!this.reuse) {
125-
this.clearView();
143+
if (!rendered) {
144+
if (!this.reuse) {
145+
this.clearView();
146+
}
147+
148+
return this.loadAndRender(current.route);
126149
}
127150

128-
return this.loadAndRender(current.route);
151+
return of(null);
152+
} else if (rendered) {
153+
return of(this.clearView());
129154
}
130155

131156
return of(null);
132-
} else if (rendered) {
133-
return of(this.clearView());
134-
}
135-
136-
return of(null);
137-
})
138-
);
139-
140-
merge(activeRoute$).pipe(takeUntil(this.destroy$)).subscribe();
157+
}),
158+
takeUntil(this.destroy$)
159+
)
160+
.subscribe();
141161
}
142162

143163
ngOnDestroy() {
@@ -206,12 +226,12 @@ export class RouteComponent implements OnInit, OnDestroy {
206226

207227
private showTemplate() {
208228
setTimeout(() => {
209-
this._shouldRender$.next(true);
229+
this.updateState({ shouldRender: true });
210230
});
211231
}
212232

213233
private hideTemplate() {
214-
this._shouldRender$.next(false);
234+
this.updateState({ shouldRender: false });
215235
}
216236

217237
private clearView() {
@@ -226,4 +246,8 @@ export class RouteComponent implements OnInit, OnDestroy {
226246
const trimmed = path.trim();
227247
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
228248
}
249+
250+
private updateState(newState: Partial<State>) {
251+
this.state$.next({ ...this.state$.value, ...newState });
252+
}
229253
}

libs/angular-routing/src/lib/router.component.ts

+28-15
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import {
55
OnInit,
66
OnDestroy,
77
} from '@angular/core';
8-
import { Location } from '@angular/common';
98
import { combineLatest, Subject, BehaviorSubject } from 'rxjs';
109
import {
1110
tap,
1211
takeUntil,
1312
distinctUntilChanged,
14-
scan,
1513
debounceTime,
14+
map,
1615
} from 'rxjs/operators';
1716

1817
import { pathToRegexp, match } from 'path-to-regexp';
@@ -22,22 +21,31 @@ import { Router } from './router.service';
2221
import { compareParams, Params } from './route-params.service';
2322
import { compareRoutes } from './utils/compare-routes';
2423

24+
interface State {
25+
activeRoute: ActiveRoute | null;
26+
routes: Route[];
27+
}
28+
2529
@Component({
2630
// tslint:disable-next-line:component-selector
2731
selector: 'router',
2832
template: '<ng-content></ng-content>',
2933
})
3034
export class RouterComponent implements OnInit, OnDestroy {
3135
private destroy$ = new Subject();
32-
33-
private _activeRoute$ = new BehaviorSubject<ActiveRoute>(null);
34-
readonly activeRoute$ = this._activeRoute$.pipe(
35-
distinctUntilChanged(this.compareActiveRoutes)
36+
private readonly state$ = new BehaviorSubject<State>({
37+
activeRoute: null,
38+
routes: [],
39+
});
40+
41+
readonly activeRoute$ = this.state$.pipe(
42+
map((state) => state.activeRoute),
43+
distinctUntilChanged(this.compareActiveRoutes),
44+
takeUntil(this.destroy$)
3645
);
37-
38-
private _routes$ = new BehaviorSubject<Route[]>([]);
39-
readonly routes$ = this._routes$.pipe(
40-
scan((routes, route) => routes.concat(route).sort(compareRoutes))
46+
readonly routes$ = this.state$.pipe(
47+
map((state) => state.routes),
48+
takeUntil(this.destroy$)
4149
);
4250

4351
public basePath = '';
@@ -52,7 +60,6 @@ export class RouterComponent implements OnInit, OnDestroy {
5260

5361
constructor(
5462
private router: Router,
55-
private location: Location,
5663
@SkipSelf() @Optional() public parentRouterComponent: RouterComponent
5764
) {}
5865

@@ -108,17 +115,19 @@ export class RouterComponent implements OnInit, OnDestroy {
108115
});
109116

110117
route.matcher = route.matcher || routeRegex;
111-
this._routes$.next([route]);
118+
119+
const routes = this.state$.value.routes;
120+
this.updateState({ routes: routes.concat(route).sort(compareRoutes) });
112121

113122
return route;
114123
}
115124

116-
setActiveRoute(active: ActiveRoute) {
117-
this._activeRoute$.next(active);
125+
setActiveRoute(activeRoute: ActiveRoute) {
126+
this.updateState({ activeRoute });
118127
}
119128

120129
normalizePath(path: string) {
121-
return this.location.normalize(path);
130+
return this.router.normalizePath(path);
122131
}
123132

124133
ngOnDestroy() {
@@ -142,4 +151,8 @@ export class RouterComponent implements OnInit, OnDestroy {
142151
previous.route.options.exact === current.route.options.exact
143152
);
144153
}
154+
155+
private updateState(newState: Partial<State>) {
156+
this.state$.next({ ...this.state$.value, ...newState });
157+
}
145158
}

libs/angular-routing/src/lib/router.service.ts

+38-28
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,42 @@ import { Injectable } from '@angular/core';
22
import { PlatformLocation, Location } from '@angular/common';
33

44
import { BehaviorSubject } from 'rxjs';
5-
import { distinctUntilChanged } from 'rxjs/operators';
5+
import { distinctUntilChanged, map } from 'rxjs/operators';
66

77
import * as queryString from 'query-string';
88

99
import { UrlParser } from './url-parser';
1010
import { Params, compareParams } from './route-params.service';
1111

12+
interface State {
13+
url: string;
14+
queryParams: Params;
15+
hash: string;
16+
}
17+
1218
@Injectable({
1319
providedIn: 'root',
1420
})
1521
export class Router {
16-
private _url$ = new BehaviorSubject<string>(this.location.path());
17-
readonly url$ = this._url$.pipe(distinctUntilChanged());
18-
19-
private _queryParams$ = new BehaviorSubject<Params>({});
20-
readonly queryParams$ = this._queryParams$.pipe(
22+
private readonly state$ = new BehaviorSubject<State>({
23+
url: this.location.path(),
24+
queryParams: {},
25+
hash: '',
26+
});
27+
28+
readonly url$ = this.state$.pipe(
29+
map((state) => state.url),
30+
distinctUntilChanged()
31+
);
32+
readonly hash$ = this.state$.pipe(
33+
map((state) => state.hash),
34+
distinctUntilChanged()
35+
);
36+
readonly queryParams$ = this.state$.pipe(
37+
map((state) => state.queryParams),
2138
distinctUntilChanged(compareParams)
2239
);
2340

24-
private _hash$ = new BehaviorSubject<string>('');
25-
readonly hash$ = this._hash$.pipe(distinctUntilChanged());
26-
2741
constructor(
2842
private location: Location,
2943
private platformLocation: PlatformLocation,
@@ -53,6 +67,7 @@ export class Router {
5367
if (!url.startsWith('/')) {
5468
url = this.urlParser.joinUrls(this.location.path(), url);
5569
}
70+
5671
return (
5772
url +
5873
(queryParams ? `?${queryString.stringify(queryParams)}` : '') +
@@ -64,17 +79,6 @@ export class Router {
6479
return this.location.prepareExternalUrl(url);
6580
}
6681

67-
private getLocation() {
68-
return this.platformLocation.href;
69-
}
70-
71-
private nextState(url: string) {
72-
const parsedUrl = this._parseUrl(url);
73-
this._nextUrl(parsedUrl.pathname);
74-
this._nextQueryParams(this.parseSearchParams(parsedUrl.searchParams));
75-
this._nextHash(parsedUrl.hash ? parsedUrl.hash.split('#')[0] : '');
76-
}
77-
7882
parseSearchParams(searchParams: URLSearchParams) {
7983
const queryParams: Params = {};
8084

@@ -85,19 +89,25 @@ export class Router {
8589
return queryParams;
8690
}
8791

88-
private _parseUrl(path: string): URL {
89-
return this.urlParser.parse(path);
92+
normalizePath(path: string) {
93+
return this.location.normalize(path);
9094
}
9195

92-
private _nextUrl(url: string) {
93-
this._url$.next(url);
96+
private getLocation() {
97+
return this.platformLocation.href;
9498
}
9599

96-
private _nextQueryParams(params: Params) {
97-
this._queryParams$.next(params);
100+
private nextState(url: string) {
101+
const parsedUrl = this._parseUrl(url);
102+
103+
this.state$.next({
104+
url: parsedUrl.pathname,
105+
queryParams: this.parseSearchParams(parsedUrl.searchParams),
106+
hash: parsedUrl.hash ? parsedUrl.hash.split('#')[0] : '',
107+
});
98108
}
99109

100-
private _nextHash(hash: string) {
101-
this._hash$.next(hash);
110+
private _parseUrl(path: string): URL {
111+
return this.urlParser.parse(path);
102112
}
103113
}

0 commit comments

Comments
 (0)