Skip to content

Commit

Permalink
feat: add active link directive
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonroberts committed Jun 20, 2020
1 parent 17cd7ff commit a315ac7
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 5 deletions.
121 changes: 121 additions & 0 deletions projects/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();
}
}
2 changes: 2 additions & 0 deletions projects/router/src/lib/router.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { RouterComponent } from './router.component';
import { RouteComponent } from './route.component';
import { RouteComponentTemplate } from './route-component.directive';
import { LinkActive } from './link-active';
import { LinkTo } from './link.component';
import { UrlParser } from './url-parser';
import { QueryParams } from './route-params.service';
Expand All @@ -16,6 +17,7 @@ import { Router } from './router.service';
const components = [
RouterComponent,
RouteComponent,
LinkActive,
LinkTo,
RouteComponentTemplate,
];
Expand Down
2 changes: 1 addition & 1 deletion projects/router/src/lib/router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Params } from './route-params.service';
providedIn: 'root',
})
export class Router {
private _url$ = new BehaviorSubject<string>(this.getLocation());
private _url$ = new BehaviorSubject<string>(this.location.path());
readonly url$ = this._url$.pipe(distinctUntilChanged());

private _queryParams$ = new BehaviorSubject<Params>({});
Expand Down
12 changes: 8 additions & 4 deletions src/app/core/components/layout/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';

import { RouterModule } from '@blog/router';
import { RouterModule, Router } from '@blog/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

Expand Down Expand Up @@ -60,11 +60,12 @@ import { PageNotFoundComponentModule } from '../page-not-found/page-not-found.co
<a linkTo="/">Brandon Roberts</a>
<div class="social">
<a *ngIf="!(isHandset$ | async)" linkTo="/talks">Talks</a>
<a *ngIf="!(isHandset$ | async)" linkTo="/talks" linkActive="active">Talks</a>
<a
*ngIf="!(isHandset$ | async)"
linkTo="/about"
linkActive="active"
[queryParams]="{ test: 123 }"
fragment="bottom"
>About</a
Expand Down Expand Up @@ -139,7 +140,7 @@ import { PageNotFoundComponentModule } from '../page-not-found/page-not-found.co
margin-left: 16px;
}
.social a:hover {
.social a:hover, a.active {
opacity: 0.8;
}
Expand All @@ -165,7 +166,10 @@ export class LayoutComponent {
.observe(Breakpoints.Handset)
.pipe(map((result) => result.matches));

constructor(private breakpointObserver: BreakpointObserver) {}
constructor(
private breakpointObserver: BreakpointObserver,
public router: Router
) {}
}

@NgModule({
Expand Down

0 comments on commit a315ac7

Please sign in to comment.