Created
January 12, 2018 13:30
-
-
Save bennadel/d51b59caca0757dfa9b991a6e299b600 to your computer and use it in GitHub Desktop.
Creating A Jump-To-Anchor Fragment Polyfill In Angular 5.2.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> — | |
<a routerLink="/app/a" fragment="top">A View #top</a> — | |
<a routerLink="/app/a" fragment="bottom">A View #bottom</a><br /> | |
<a routerLink="/app/b">B View</a> — | |
<a routerLink="/app/b" fragment="top">B View #top</a> — | |
<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 { | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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