Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created January 12, 2018 13:30
Show Gist options
  • Save bennadel/d51b59caca0757dfa9b991a6e299b600 to your computer and use it in GitHub Desktop.
Save bennadel/d51b59caca0757dfa9b991a6e299b600 to your computer and use it in GitHub Desktop.
Creating A Jump-To-Anchor Fragment Polyfill In Angular 5.2.0
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "a-view",
styleUrls: [ "./a-view.component.less" ],
template:
`
<hr id="top" />
<p>
<strong>A View</strong>
</p>
<p class="content">
<a routerLink="." fragment="bottom">Jump to bottom</a>
</p>
<a name="bottom"></a>
<p>
This is the bottom of <strong>A-view</strong>.
<a routerLink="." fragment="top">Back to top</a>.
</p>
`
})
export class AViewComponent {
// ...
}
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<p>
<a routerLink="/">Home View</a><br />
<br />
<a routerLink="/app/a">A View</a> &mdash;
<a routerLink="/app/a" fragment="top">A View #top</a> &mdash;
<a routerLink="/app/a" fragment="bottom">A View #bottom</a><br />
<a routerLink="/app/b">B View</a> &mdash;
<a routerLink="/app/b" fragment="top">B View #top</a> &mdash;
<a routerLink="/app/b" fragment="bottom">B View #bottom</a><br />
</p>
<p>
<strong>Home View</strong>
</p>
<router-outlet></router-outlet>
`
})
export class AppComponent {
// ...
}
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { AViewComponent } from "./a-view.component";
import { BViewComponent } from "./b-view.component";
import { FragmentPolyfillModule } from "./fragment-polyfill.module";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var routes: Routes = [
{
path: "app",
children: [
{
path: "a",
component: AViewComponent
},
{
path: "b",
component: BViewComponent
}
]
},
// Redirect from the root to the "/app" prefix (this makes other features, like
// secondary outlets) easier to implement later on.
{
path: "",
pathMatch: "full",
redirectTo: "app"
}
];
@NgModule({
bootstrap: [
AppComponent
],
imports: [
BrowserModule,
FragmentPolyfillModule.forRoot({
smooth: true
}),
RouterModule.forRoot(
routes,
{
// Tell the router to use the HashLocationStrategy.
useHash: true,
enableTracing: false
}
)
],
declarations: [
AppComponent,
AViewComponent,
BViewComponent
],
providers: [
// CAUTION: We don't need to specify the LocationStrategy because we are setting
// the "useHash" property in the Router module above.
// --
// {
// provide: LocationStrategy,
// useClass: HashLocationStrategy
// }
]
})
export class AppModule {
// ...
}
// Import the core angular services.
import { ActivatedRoute } from "@angular/router";
import { Directive } from "@angular/core";
import { ElementRef } from "@angular/core";
import { Inject } from "@angular/core";
import { InjectionToken } from "@angular/core";
import { ModuleWithProviders } from "@angular/core";
import { NgModule } from "@angular/core";
import { OnDestroy } from "@angular/core";
import { OnInit } from "@angular/core";
import { Subscription } from "rxjs/Subscription";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export interface WindowScrollerOptions {
smooth: boolean;
}
export var WINDOW_SCROLLER_OPTIONS = new InjectionToken<WindowScrollerOptions>( "WindowScroller.Options" );
// I provide the dependency-injection token for the window-scroller so that it can be
// more easily injected into the FragmentTarget directive. This allows other developers
// to provide an override that implements this Type without have to deal with the silly
// @Inject() decorator.
export abstract class WindowScroller {
abstract scrollIntoView( elementRef: ElementRef ) : void;
}
// I provide an implementation for scrolling a given Element Reference into view. By
// default, it uses the native .scrollIntoView() method; but, it can be overridden to
// use something like a jQuery plug-in, or other custom implementation.
class NativeWindowScroller implements WindowScroller {
private behavior: "auto" | "smooth";
private timer: number;
// I initialize the window scroller implementation.
public constructor( @Inject( WINDOW_SCROLLER_OPTIONS ) options: WindowScrollerOptions ) {
this.behavior = ( options.smooth ? "smooth" : "auto" );
this.timer = null;
}
// ---
// PUBLIC METHODS.
// ---
// I scroll the given ElementRef into the client's viewport.
public scrollIntoView( elementRef: ElementRef ) : void {
// NOTE: There is an odd race-condition that I cannot figure out. The initial
// scrollToView() will not work when the BROWSER IS REFRESHED. It will work if
// the page is opened in a new tab; it only fails on refresh (WAT?!). To fix this
// peculiarity, I'm putting the first scroll operation behind a timer. The rest
// of the scroll operations will initiate synchronously.
if ( this.timer ) {
this.doScroll( elementRef );
} else {
this.timer = setTimeout(
() : void => {
this.doScroll( elementRef );
},
0
);
}
}
// ---
// PRIVATE METHOD.
// ---
// I perform the scrolling of the viewport.
private doScroll( elementRef: ElementRef ) : void {
elementRef.nativeElement.scrollIntoView({
behavior: this.behavior,
block: "start"
});
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Directive({
selector: "[id], a[name]",
inputs: [ "id", "name" ]
})
export class FragmentTargetDirective implements OnInit, OnDestroy {
public id: string;
public name: string;
private activatedRoute: ActivatedRoute;
private elementRef: ElementRef;
private fragmentSubscription: Subscription;
private windowScroller: WindowScroller;
// I initialize the fragment-target directive.
constructor(
activatedRoute: ActivatedRoute,
elementRef: ElementRef,
windowScroller: WindowScroller
) {
this.activatedRoute = activatedRoute;
this.elementRef = elementRef;
this.windowScroller = windowScroller;
this.id = null;
this.fragmentSubscription = null;
this.name = null;
}
// ---
// PUBLIC METHODS.
// ---
// I get called once when the directive is being destroyed.
public ngOnDestroy() : void {
( this.fragmentSubscription ) && this.fragmentSubscription.unsubscribe();
}
// I get called once after the inputs have been bound for the first time.
public ngOnInit() : void {
this.fragmentSubscription = this.activatedRoute.fragment.subscribe(
( fragment: string ) : void => {
if ( ! fragment ) {
return;
}
if (
( fragment !== this.id ) &&
( fragment !== this.name )
) {
return;
}
this.windowScroller.scrollIntoView( this.elementRef );
}
);
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface ModuleOptions {
smooth?: boolean;
}
@NgModule({
exports: [
FragmentTargetDirective
],
declarations: [
FragmentTargetDirective
]
})
export class FragmentPolyfillModule {
static forRoot( options?: ModuleOptions ) : ModuleWithProviders {
return({
ngModule: FragmentPolyfillModule,
providers: [
{
provide: WINDOW_SCROLLER_OPTIONS,
useValue: {
smooth: ( ( options && options.smooth ) || false )
}
},
{
provide: WindowScroller,
useClass: NativeWindowScroller
}
]
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment