Skip to content

Commit

Permalink
feat: add router lib
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonroberts committed Jun 23, 2020
1 parent edf9975 commit c2502f6
Show file tree
Hide file tree
Showing 22 changed files with 859 additions and 4 deletions.
34 changes: 32 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
{
"version": 1,
"projects": {},
"projects": {
"router": {
"projectType": "library",
"root": "libs/router",
"sourceRoot": "libs/router/src",
"prefix": "reactiveangular",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/router/tsconfig.lib.json",
"libs/router/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**", "!libs/router/**/*"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/router/jest.config.js",
"tsConfig": "libs/router/tsconfig.spec.json",
"passWithNoTests": true,
"setupFile": "libs/router/src/test-setup.ts"
}
}
},
"schematics": {}
}
},
"cli": {
"defaultCollection": "@nrwl/angular"
},
Expand All @@ -12,5 +41,6 @@
"@nrwl/angular:library": {
"unitTestRunner": "jest"
}
}
},
"defaultProject": "router"
}
7 changes: 7 additions & 0 deletions libs/router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# router

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test router` to execute the unit tests.
10 changes: 10 additions & 0 deletions libs/router/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
name: 'router',
preset: '../../jest.config.js',
coverageDirectory: '../../coverage/libs/router',
snapshotSerializers: [
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
'jest-preset-angular/build/AngularSnapshotSerializer.js',
'jest-preset-angular/build/HTMLCommentSerializer.js',
],
};
12 changes: 12 additions & 0 deletions libs/router/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Public API Surface of router
*/

export * from './lib/router.service';
export * from './lib/router.component';
export * from './lib/route';
export * from './lib/route.component';
export * from './lib/router.module';
export * from './lib/route-params.service';
export * from './lib/link.component';
export * from './lib/url-parser';
121 changes: 121 additions & 0 deletions libs/router/src/lib/link-active.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
AfterContentInit,
Directive,
ElementRef,
Input,
OnDestroy,
QueryList,
Renderer2,
Optional,
Inject,
ContentChildren,
EventEmitter
} from '@angular/core';
import { LinkTo } from './link.component';
import { Router } from './router.service';
import { from, Subject, Subscription, EMPTY, of, merge, combineLatest } from 'rxjs';
import { mergeAll, map, withLatestFrom, takeUntil, startWith, switchMapTo, mapTo, mergeMap, tap, combineAll, toArray, switchMap, switchAll } from 'rxjs/operators';

export interface LinkActiveOptions {
exact: boolean;
}

export const LINK_ACTIVE_OPTIONS: LinkActiveOptions = {
exact: true
};

/**
* The LinkActive directive toggles classes on elements that contain an active linkTo directive
*
* <a linkActive="active" linkTo="/home/page">Home Page</a>
* <ol>
* <li linkActive="active" *ngFor="var link of links">
* <a [linkTo]="'/link/' + link.id">{{ link.title }}</a>
* </li>
* </ol>
*/
@Directive({ selector: '[linkActive]' })
export class LinkActive implements AfterContentInit, OnDestroy {
@ContentChildren(LinkTo, { descendants: true }) public links: QueryList<LinkTo>;
@Input('linkActive') activeClass: string = 'active';
@Input() activeOptions: LinkActiveOptions;
private _activeOptions: LinkActiveOptions = { exact: true };
private _destroy$ = new Subject();
private _linksSub!: Subscription;

constructor(
public element: ElementRef,
public router: Router,
public renderer: Renderer2,
@Optional()
@Inject(LINK_ACTIVE_OPTIONS)
private defaultActiveOptions: LinkActiveOptions,
@Optional() private link: LinkTo
) { }

ngAfterContentInit() {
if (this.defaultActiveOptions && !this.activeOptions) {
this._activeOptions = this.defaultActiveOptions;
} else if (this.activeOptions) {
this._activeOptions = this.activeOptions;
}

this.links.changes.subscribe(() => this.collectLinks());
this.collectLinks();
}

ngOnChanges() {
this.collectLinks();
}

collectLinks() {
if (this._linksSub) {
this._linksSub.unsubscribe();
}

const contentLinks$ = this.links ? this.links.toArray().map(link => link.hrefUpdated.pipe(startWith(link.linkHref), mapTo(link.linkHref))) : [];
const link$ = this.link ? this.link.hrefUpdated.pipe(startWith(this.link.linkHref), mapTo(this.link.linkHref)) : of('');
const router$ = this.router.url$
.pipe(
map(path => this.router.getExternalUrl(path || '/')));

const observables$ = [router$, link$, ...contentLinks$];

this._linksSub = combineLatest(observables$).pipe(
takeUntil(this._destroy$)
).subscribe(([path, link, ...links]) => {
this.checkActive([...links, link], path);
});
}

checkActive(linkHrefs: string[], path: string) {
let active = linkHrefs.reduce((isActive, current) => {
const [href] = current.split('?');

if (this._activeOptions.exact) {
isActive = isActive ? isActive : href === path;
} else {
isActive = isActive ? isActive : path.startsWith(href);
}

return isActive;
}, false);

this.updateClasses(active);
}

updateClasses(active: boolean) {
let activeClasses = this.activeClass.split(' ');
activeClasses.forEach((activeClass) => {
if (active) {
this.renderer.addClass(this.element.nativeElement, activeClass);
} else {
this.renderer.removeClass(this.element.nativeElement, activeClass);
}
});
}

ngOnDestroy() {
this._destroy$.next();
}
}
89 changes: 89 additions & 0 deletions libs/router/src/lib/link.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Directive,
HostBinding,
HostListener,
Input,
Output,
EventEmitter,
} from '@angular/core';
import { Router } from './router.service';
import { Params } from './route-params.service';

/**
* The LinkTo directive links to routes in your app
*
* Links are pushed to the `Router` service to trigger a route change.
* Query params can be represented as an object or a string of names/values
*
* <a linkTo="/home/page" [queryParams]="{ id: 123 }">Home Page</a>
* <a [linkTo]="'/pages' + page.id">Page 1</a>
*/
@Directive({ selector: 'a[linkTo]' })
export class LinkTo {
@Input() target: string;
@HostBinding('href') linkHref: string;

@Input() set linkTo(href: string) {
this._href = href;
this._updateHref();
}

@Input() set queryParams(params: Params) {
this._query = params;
this._updateHref();
}

@Input() set fragment(hash: string) {
this._hash = hash;
this._updateHref();
}

@Output() hrefUpdated: EventEmitter<string> = new EventEmitter<string>();

private _href: string;
private _query: Params;
private _hash: string;

constructor(private router: Router) {}

/**
* Handles click events on the associated link
* Prevents default action for non-combination click events without a target
*/
@HostListener('click', ['$event'])
onClick(event: any) {
if (!this._comboClick(event) && !this.target) {
this.router.go(this._href, this._query, this._hash);

event.preventDefault();
}
}

private _updateHref() {
let path = this._cleanUpHref(this._href);

let url = this.router.serializeUrl(path, this._query, this._hash);
this.linkHref = url;

this.hrefUpdated.emit(this.linkHref);
}

/**
* Determines whether the click event happened with a combination of other keys
*/
private _comboClick(event) {
let buttonEvent = event.which || event.button;

return buttonEvent > 1 || event.ctrlKey || event.metaKey || event.shiftKey;
}

private _cleanUpHref(href: string = ''): string {
// Check for trailing slashes in the path
while (href.length > 1 && href.substr(-1) === '/') {
// Remove trailing slashes
href = href.substring(0, href.length - 1);
}

return href;
}
}
8 changes: 8 additions & 0 deletions libs/router/src/lib/route-component.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Directive, Input } from '@angular/core';

@Directive({
selector: '[routeComponent]',
})
export class RouteComponentTemplate {
@Input() routeComponent: any;
}
9 changes: 9 additions & 0 deletions libs/router/src/lib/route-params.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from 'rxjs';

export interface Params {
[param: string]: any;
}

export class RouteParams<T extends Params = Params> extends Observable<T> {}

export class QueryParams<T extends Params = Params> extends Observable<T> {}

0 comments on commit c2502f6

Please sign in to comment.