Navigation using routerLink on an <a> element occurs immediately, but there are situations where delaying navigation after clicking a link would be benificial

Flickering Transitions

Transitions on sliding menus can appear to flicker if they are transitioning simultaneouly with the rendering of the underlying components e.g. new pages being loaded while a menu is still transitioning off the screen. Allowing the transiiton to complete before the navigation occurs removes the chance of flickering

I wanted to have this delay functionality while still having the declarative ease of use of Angular's inbuilt routerLink directive. I extended the current functionality with an additional @Input for delaying the navigation by a given nubmer of milliseconds. The extended directive is available as an npm module

Extending Directives

When extending Angular directives we need to use a custom selector to differentiate between the built-in and extended versions. In this case I am using a prefix on all directives and components (which I set when initializing an Angular CLI project)

@Directive({
    selector: 'a[bcRouterLink]'
})
export class RouterLinkWithHrefDelay extends RouterLinkWithHref implements OnDestroy {

To maintain the functionality of the base class RouterLinkWithHref we have to set the RouterLinkWithHref.routerLink input value when we recieve the bcRouterLink input value from the template:

<a [bcRouterLink]="['/page-one']" bcRouterLinkActive="active" [navigationDelay]="250">
@Input()
set bcRouterLink(commands: any[] | string) {
    this.routerLink = commands;
}

We need to override the @HostListener click event handler of RouterLinkWithHref, and add in our delay logic. For this I am using an Observable timer operator:

import { timer } from 'rxjs/observable/timer';

// v1.1.0 is using lettable Rxjs timer
// removed: Observable.timer(this.navigationDelay)
this.timerSubscription = timer(this.navigationDelay)
    .subscribe(t => {
        this.timerSubscription.unsubscribe();
        super.onClick(button, ctrlKey, metaKey, shiftKey);
    });

The Observable.timer completes after it's first event if no period argument is supplied - I'm calling unsubscribe here because..... see Ben Lesh's article on not calling unsubscribe too often. I just realised that unsubscribe isn't needed while writing this article - next release!! 😉

// Observable.timer function signature
timer(initialDelay: number | Date, period: number, scheduler: Scheduler): Observable

The this.timerSubscription is also invoked if necessary in ngOnDestroy, for the case that the <a> element is removed from the view before the timer completes (this scenario would probably be very rare - but it can be emulated using fakeasync tests)

The routerLinkActive directive (easily confused with an @Input of the routerLink directive) also needs to be extended to work alongside the extended RouterLinkWithHref.

Remember to look carefully at the source files directly and not just the .d.ts definitions when extending Angular components

RouterLinkActive queries the template for a list of anchor elements with directives of type RouterLinkWithHref. We need to extend RouterLinkActive and query for the extended component:

@ContentChildren(RouterLinkWithHref, {descendants: true})
linksWithHrefs: QueryList<RouterLinkWithHref>;

// becomes ->

@ContentChildren(RouterLinkWithHrefDelay, { descendants: true })
linksWithHrefs: QueryList<RouterLinkWithHrefDelay>;

Usage

app.component.html

<div>
    <a [bcRouterLink]="['/page-one']" bcRouterLinkActive="active" [navigationDelay]="250">
        Page One
    </a>
    <a bcRouterLink="/page-two" bcRouterLinkActive="active" [navigationDelay]="250">
        Page Two
    </a>
    <router-outlet></router-outlet>
</div>

app.module.ts

import { AppComponent } from './app.component';

import { RouterLinkDelayModule } from '@bcodes/ngx-routerlink-delay';
import { PageOneComponent } from './pages/page-one/page-one.component';
import { PageTwoComponent } from './pages/page-two/page-two.component';

@NgModule({
    declarations: [
        AppComponent,
        PageOneComponent,
        PageTwoComponent
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        RouterLinkDelayModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

npm and GitHub

The directive is available as an npm module

npm install @bcodes/ngx-routerlink-delay

The source and tests can be seen on GitHub

Brian Bishop

Dublin, Ireland
v7.2.15+sha.e7897e1