Skip to content

Commit c2502f6

Browse files
feat: add router lib
1 parent edf9975 commit c2502f6

22 files changed

+859
-4
lines changed

angular.json

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
{
22
"version": 1,
3-
"projects": {},
3+
"projects": {
4+
"router": {
5+
"projectType": "library",
6+
"root": "libs/router",
7+
"sourceRoot": "libs/router/src",
8+
"prefix": "reactiveangular",
9+
"architect": {
10+
"lint": {
11+
"builder": "@angular-devkit/build-angular:tslint",
12+
"options": {
13+
"tsConfig": [
14+
"libs/router/tsconfig.lib.json",
15+
"libs/router/tsconfig.spec.json"
16+
],
17+
"exclude": ["**/node_modules/**", "!libs/router/**/*"]
18+
}
19+
},
20+
"test": {
21+
"builder": "@nrwl/jest:jest",
22+
"options": {
23+
"jestConfig": "libs/router/jest.config.js",
24+
"tsConfig": "libs/router/tsconfig.spec.json",
25+
"passWithNoTests": true,
26+
"setupFile": "libs/router/src/test-setup.ts"
27+
}
28+
}
29+
},
30+
"schematics": {}
31+
}
32+
},
433
"cli": {
534
"defaultCollection": "@nrwl/angular"
635
},
@@ -12,5 +41,6 @@
1241
"@nrwl/angular:library": {
1342
"unitTestRunner": "jest"
1443
}
15-
}
44+
},
45+
"defaultProject": "router"
1646
}

libs/router/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# router
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Running unit tests
6+
7+
Run `nx test router` to execute the unit tests.

libs/router/jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
name: 'router',
3+
preset: '../../jest.config.js',
4+
coverageDirectory: '../../coverage/libs/router',
5+
snapshotSerializers: [
6+
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
7+
'jest-preset-angular/build/AngularSnapshotSerializer.js',
8+
'jest-preset-angular/build/HTMLCommentSerializer.js',
9+
],
10+
};

libs/router/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Public API Surface of router
3+
*/
4+
5+
export * from './lib/router.service';
6+
export * from './lib/router.component';
7+
export * from './lib/route';
8+
export * from './lib/route.component';
9+
export * from './lib/router.module';
10+
export * from './lib/route-params.service';
11+
export * from './lib/link.component';
12+
export * from './lib/url-parser';

libs/router/src/lib/link-active.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
AfterContentInit,
3+
Directive,
4+
ElementRef,
5+
Input,
6+
OnDestroy,
7+
QueryList,
8+
Renderer2,
9+
Optional,
10+
Inject,
11+
ContentChildren,
12+
EventEmitter
13+
} from '@angular/core';
14+
import { LinkTo } from './link.component';
15+
import { Router } from './router.service';
16+
import { from, Subject, Subscription, EMPTY, of, merge, combineLatest } from 'rxjs';
17+
import { mergeAll, map, withLatestFrom, takeUntil, startWith, switchMapTo, mapTo, mergeMap, tap, combineAll, toArray, switchMap, switchAll } from 'rxjs/operators';
18+
19+
export interface LinkActiveOptions {
20+
exact: boolean;
21+
}
22+
23+
export const LINK_ACTIVE_OPTIONS: LinkActiveOptions = {
24+
exact: true
25+
};
26+
27+
/**
28+
* The LinkActive directive toggles classes on elements that contain an active linkTo directive
29+
*
30+
* <a linkActive="active" linkTo="/home/page">Home Page</a>
31+
* <ol>
32+
* <li linkActive="active" *ngFor="var link of links">
33+
* <a [linkTo]="'/link/' + link.id">{{ link.title }}</a>
34+
* </li>
35+
* </ol>
36+
*/
37+
@Directive({ selector: '[linkActive]' })
38+
export class LinkActive implements AfterContentInit, OnDestroy {
39+
@ContentChildren(LinkTo, { descendants: true }) public links: QueryList<LinkTo>;
40+
@Input('linkActive') activeClass: string = 'active';
41+
@Input() activeOptions: LinkActiveOptions;
42+
private _activeOptions: LinkActiveOptions = { exact: true };
43+
private _destroy$ = new Subject();
44+
private _linksSub!: Subscription;
45+
46+
constructor(
47+
public element: ElementRef,
48+
public router: Router,
49+
public renderer: Renderer2,
50+
@Optional()
51+
@Inject(LINK_ACTIVE_OPTIONS)
52+
private defaultActiveOptions: LinkActiveOptions,
53+
@Optional() private link: LinkTo
54+
) { }
55+
56+
ngAfterContentInit() {
57+
if (this.defaultActiveOptions && !this.activeOptions) {
58+
this._activeOptions = this.defaultActiveOptions;
59+
} else if (this.activeOptions) {
60+
this._activeOptions = this.activeOptions;
61+
}
62+
63+
this.links.changes.subscribe(() => this.collectLinks());
64+
this.collectLinks();
65+
}
66+
67+
ngOnChanges() {
68+
this.collectLinks();
69+
}
70+
71+
collectLinks() {
72+
if (this._linksSub) {
73+
this._linksSub.unsubscribe();
74+
}
75+
76+
const contentLinks$ = this.links ? this.links.toArray().map(link => link.hrefUpdated.pipe(startWith(link.linkHref), mapTo(link.linkHref))) : [];
77+
const link$ = this.link ? this.link.hrefUpdated.pipe(startWith(this.link.linkHref), mapTo(this.link.linkHref)) : of('');
78+
const router$ = this.router.url$
79+
.pipe(
80+
map(path => this.router.getExternalUrl(path || '/')));
81+
82+
const observables$ = [router$, link$, ...contentLinks$];
83+
84+
this._linksSub = combineLatest(observables$).pipe(
85+
takeUntil(this._destroy$)
86+
).subscribe(([path, link, ...links]) => {
87+
this.checkActive([...links, link], path);
88+
});
89+
}
90+
91+
checkActive(linkHrefs: string[], path: string) {
92+
let active = linkHrefs.reduce((isActive, current) => {
93+
const [href] = current.split('?');
94+
95+
if (this._activeOptions.exact) {
96+
isActive = isActive ? isActive : href === path;
97+
} else {
98+
isActive = isActive ? isActive : path.startsWith(href);
99+
}
100+
101+
return isActive;
102+
}, false);
103+
104+
this.updateClasses(active);
105+
}
106+
107+
updateClasses(active: boolean) {
108+
let activeClasses = this.activeClass.split(' ');
109+
activeClasses.forEach((activeClass) => {
110+
if (active) {
111+
this.renderer.addClass(this.element.nativeElement, activeClass);
112+
} else {
113+
this.renderer.removeClass(this.element.nativeElement, activeClass);
114+
}
115+
});
116+
}
117+
118+
ngOnDestroy() {
119+
this._destroy$.next();
120+
}
121+
}

libs/router/src/lib/link.component.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
Directive,
3+
HostBinding,
4+
HostListener,
5+
Input,
6+
Output,
7+
EventEmitter,
8+
} from '@angular/core';
9+
import { Router } from './router.service';
10+
import { Params } from './route-params.service';
11+
12+
/**
13+
* The LinkTo directive links to routes in your app
14+
*
15+
* Links are pushed to the `Router` service to trigger a route change.
16+
* Query params can be represented as an object or a string of names/values
17+
*
18+
* <a linkTo="/home/page" [queryParams]="{ id: 123 }">Home Page</a>
19+
* <a [linkTo]="'/pages' + page.id">Page 1</a>
20+
*/
21+
@Directive({ selector: 'a[linkTo]' })
22+
export class LinkTo {
23+
@Input() target: string;
24+
@HostBinding('href') linkHref: string;
25+
26+
@Input() set linkTo(href: string) {
27+
this._href = href;
28+
this._updateHref();
29+
}
30+
31+
@Input() set queryParams(params: Params) {
32+
this._query = params;
33+
this._updateHref();
34+
}
35+
36+
@Input() set fragment(hash: string) {
37+
this._hash = hash;
38+
this._updateHref();
39+
}
40+
41+
@Output() hrefUpdated: EventEmitter<string> = new EventEmitter<string>();
42+
43+
private _href: string;
44+
private _query: Params;
45+
private _hash: string;
46+
47+
constructor(private router: Router) {}
48+
49+
/**
50+
* Handles click events on the associated link
51+
* Prevents default action for non-combination click events without a target
52+
*/
53+
@HostListener('click', ['$event'])
54+
onClick(event: any) {
55+
if (!this._comboClick(event) && !this.target) {
56+
this.router.go(this._href, this._query, this._hash);
57+
58+
event.preventDefault();
59+
}
60+
}
61+
62+
private _updateHref() {
63+
let path = this._cleanUpHref(this._href);
64+
65+
let url = this.router.serializeUrl(path, this._query, this._hash);
66+
this.linkHref = url;
67+
68+
this.hrefUpdated.emit(this.linkHref);
69+
}
70+
71+
/**
72+
* Determines whether the click event happened with a combination of other keys
73+
*/
74+
private _comboClick(event) {
75+
let buttonEvent = event.which || event.button;
76+
77+
return buttonEvent > 1 || event.ctrlKey || event.metaKey || event.shiftKey;
78+
}
79+
80+
private _cleanUpHref(href: string = ''): string {
81+
// Check for trailing slashes in the path
82+
while (href.length > 1 && href.substr(-1) === '/') {
83+
// Remove trailing slashes
84+
href = href.substring(0, href.length - 1);
85+
}
86+
87+
return href;
88+
}
89+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Directive, Input } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[routeComponent]',
5+
})
6+
export class RouteComponentTemplate {
7+
@Input() routeComponent: any;
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Observable } from 'rxjs';
2+
3+
export interface Params {
4+
[param: string]: any;
5+
}
6+
7+
export class RouteParams<T extends Params = Params> extends Observable<T> {}
8+
9+
export class QueryParams<T extends Params = Params> extends Observable<T> {}

0 commit comments

Comments
 (0)