Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Vicky Ryder
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Vicky Ryder

Creating A Jump-To-Anchor Fragment Polyfill In Angular 5.2.0

By
Published in Comments (39)

On top of navigating from view-to-view, the Angular 5 Router provides a lot of advanced features like lazy-loading modules, data-resolvers, and route-guards. But, ironically, it doesn't support one of the oldest and most basic browser navigation features: jumping to an anchor farther down on a given page. The GitHub issue for this "bug" has been tracked for about a year; but, no "official" fixes have come out. As such, people have been trying to come up with work-arounds, generally revolving around subscribing to changes in the ActivatedRoute's fragment property. I, too, need a work-around for this in my Router deep-dive; so, I too wanted to try and come up with "polyfill" for this behavior.

Run this demo in my JavaScript Demos project on GitHub.

If you look at the various suggestions in the GitHub issue above, they either use an alternate syntax hack (like trying to use the "href" property); or, they add additional logic to the root component that monitors the fragment and tries to reconcile it against the checked-view content. In each of these cases, the actual code of the application has be changed in order to implement the jump-to-anchor workaround.

I wanted to try and go a different route (no pun intended). Instead of adding any new logic to my components, I wanted to see if I could create a "polyfill" approach - something that would work seamlessly behind the scenes; and then, could be easily removed once Angular added official support for fragment navigation.

To do this, I thought about what situations actually cause the browser to jump down (or up) to a different location on the selected view. As best as I can remember, the fragment-navigation will be applied if there is:

So, essentially, the following selectors under the right circumstances:

  • a[name]
  • [id]

With this in mind, we can create an Angular Directive that matches on the composite value of the two selectors:

selector: "[id], a[name]"

This directive can then monitor the active fragment value. And, if the fragment value ever matches the directive's own input value (either "id" or "name"), the directive can ask the Window to scroll its Element Reference into view. With such a directive, we could implement jump-to-anchor functionality without actually changing any of the code in our application.

To do this, I created a FragmentPolyfillModule that encapsulates the future-removable code. This module provides two core facets:

  • The ([id],a[name]) directive that binds to rendered anchors.
  • A "WindowScroller" implementation that powers the actual jump to the targeted element.

I broke these two pieces apart so that you could provide a customer scroller implementation on top of the supplied directive. Out of the box, I'm using the browser's native .scrollIntoView() method. However, you could easily provide custom logic. Or, even use something like a jQuery plug-in.

Now, before we look at the module, let's look at the application code that consumes the fragment-based navigation. First, the root component:

// 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 {
	// ...
}

As you can see, the root component uses a combination of "routerLink" and "fragment" properties to jump from view-to-view. Both "View A" and "View B" are essentially copies of each other:

// 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 {
	// ...
}

As you can see, we are again using "routerLink" and "fragment" properties to define anchor navigation. But, in this case, we're also providing two anchor implementations: one anchor tag with a "name" property and one HR tag with an "id" property.

The main take-away here should be that there is nothing special about any of these components. They consume the Router directives in a natural way - no extra code, no obvious work-arounds.

Now, let's look at the application module:

// 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 {
	// ...
}

If you look at the "imports" section, you can see that I am import the FragmentPolyfillModule:

FragmentPolyfillModule.forRoot({ smooth: true })

This module is seamlessly providing the work-around for the fragment-based navigation. Now, when Angular core eventually adds the missing functionality - assuming they don't change the syntax - all we have to do is remove the FragmentPolyfillModule from the imports section and the application continues to work as expected.

Now that we've laid the ground-work for the integration, let's take a look at the module. For the sake of simplicity, I've included all aspects of the module in a single file. The downside of this approach is that the order of the classes is not ideal. Normally, I'd love to have the NgModule at the top to give a sense of what the file is providing; but, I had to put it at the bottom so that it could consume the other classes.

The order of the classes is as follows:

  • WindowScroller - This is the physical-scrolling implementation that will be injected into the fragment-target directives.
  • FragmentTargetDirective - This is the directive that binds to fragment-targets ([id], a[name]) and requests scrolling.
  • FragmentPolyfillModule - This is the module that packages it all together.

Here's the module code:

// 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
				}
			]
		});

	}

}

Now, because this module is providing directives, it means that this module has to be imported into any other module that uses anchor tags (and wants to use the polyfill). Of course, if you import this into your application's "Shared Module" (and then re-export it), this won't be an issue.

Once this is all in place, we can use fragments in our router logic. Fragment navigation isn't something that I can easily capture in a screenshot (watch the video). But, if I open this application in the browser and click on one of the "#top" link, you can see that my browser scrolls down to the correct [id]="top" element:

Creating a jump-to-fragment polyfill for the Angular 5 router.

Again, it's much easier to see in the video; but, in this screenshot, you can see that the page has been scrolled to just above the horizontal-rule (hr). This horizontal rules has an "id" property of "top", which means that it is bound to the FragmentTargetDirective, which, in turn, triggers the scrolling.

This isn't a perfect solution. It creates a small number of subscriptions to the Fragment stream. And, there are edge-cases in which a scroll will be triggered unexpectedly (for example if you suddenly reveal an element with an ID that matches the current fragment). However, neither of these issues concern me very much. And, to me, the trade-off is quite worth not having any additional logic in the application code. Now, the polyfill can be in place for as long as it's needed; and then, can be removed when it is no longer needed.

I love that you can bind a directive to any selector in the DOM. It is such a powerful feature of Angular.

Want to use code from this post? Check out the license.

Reader Comments

7 Comments

Great idea, but could use a rewrite. Takes ages to follow and implement. It's about anchor links and shouldn't take an hour to do :) Please revise, it's really a good piece otherwise

7 Comments

OK, I've implemented it. Was actually really easy - just had to ignore all the text :P ;)

Here's a helper for the TL;DR generation:

Step 1) Import the polyfill as is. Fix the type error by removing number decelerator

Step 2) Change existing "#" href links to <a routerLink="/yourUrl/whatever" fragment="anchorName"

Step 3) Thank this guy for a really cool, nicely working polyfill

Thanks, Ben! :) Works like a charme

7 Comments

Just one more comment (it's so tempting to comment and think in Schwarzenegger's voice.."to the CHOPPER!!! everybody get to the choopppperrrrr")

Just an observation: Anchors are old-school. They have been around for decades and are super useful, even in SPAs with long pages (everyone scrolls these days on mobile, it is expected behavior, in 2005 when everyone was against scrolling I could have understood it).

It's ---incredible--- how the Angular team dropped the ball on this one. I've tried a few things before, from intercepting #s. And previously, before I dropped Ionic [just a terrible, bloated, badly documented framework - I recommend everyone to stay away from it], I did it differently again. It's just a pain to see the Angular team not fixing this faster. It's been several years since the community requested this and has been even an issue in Angular1. You cannot have a framework in which there are these weird issues popping up and not being fixed. Angular is great, and I am sticking with it, but the time it takes to create a simple navigation compared to basic html (seconds vs hours) is not acceptable.

Thanks again, Ben. Made my life much easier. The polyfill is a great approach!

7 Comments

Step 4) If you get the error below during build, just add an "export" in front of the NativeWindowScroller class in the polyfill. Ben, please correct me if I'm wrong.

ERROR in Error during template compile of 'AppModule'
References to a non-exported class are not supported in decorators but NativeWindowScroller was referenced. in 'FragmentPolyfillModule'
'FragmentPolyfillModule' references non-exported class NativeWindowScroller at src\fragment-polyfill.module.ts(33,1) Consider exporting 'NativeWindowScroller'.

7 Comments

P.S. For those who do not wish to use the "#" Path Strategy (which I guess is most, because it IS ugly and confuses users), just set

useHash: false

it will still work.

Ben, again, please correct if wrong :)
Sorry for the spam, just working through it

15,848 Comments

@Mark,

No worries -- this stuff is complicated, especially when trying to jump around a feature that should "just work." Regarding the "export" before the NativeWindowScroller, you would need that if you were breaking things up into different files. In this particular demo, it was all just in one file, so the export shouldn't be needed, as far as I know. But, if you also have it all in one file, perhaps its an Angular version issue? In any way, I appreciate you documenting the error.

It's a shame you've had trouble with Ionic. I haven't used it myself, but I've heard good things. It's on my list of things to try ... eventually :)

Speaking of trying to build in features, the next thing I'm struggling with is maintaining scroll offsets when you hit the Back / Forward button. In a "normal" web page, the browser just does this for you because it renders cached pages (my understanding). But, with an SPA framework like Angular, the browser can't do that because the framework is doing all the DOM construction.

Digging into all of this stuff has been super frustrating, but also exciting.

15,848 Comments

@Mark,

Oh, and it's _never_ wrong to want to yell, "get to the choopppperrrrr!".

Do it! Do it! Come on, kill me! I'm over here!

15,848 Comments

@Mark,

Oh, and regarding "useHash", in the Router configuration, you are correct. This feature is broken with both the hash and non-hash driven navigation. And, the polyfill should work in both cases (from the brief testing I did).

As an aside, the reason I use "useHash" for all my demos is that they have to run on GitHub, where I have no server. So, if I used the pushState approach, it would work on first load, but would result in a 404 on any page refresh. So, I agree that it is less attractive; but, is somewhat necessary for my particular context.

7 Comments

@Ben,

honestly, Ionic was probably the biggest waste of time in my entire life. I started with it about 2 years ago. Went through the beta phase of ionic 2, terrible, terrible documentation, buggy code etc. It has gotten better over time, but honestly, I don't see any purpose in keeping it. It is basically an accumulation of things like hammer, swiper etc. and then adds A TON of bugs to it, that you struggle to fix because of the bad documentation and bloat. It took me a week to get rid of it, but I saved more than 300KB! and still have most of the things (e.g. swiper) I need. Incredible. Time. Waste. And. Bloat. And. Bugs!

For the few components it offers (you know, the occasional cute looking switch button) it is so not worth it. Just design it yourself, it's not that hard, and you save so much time and file size.

The problem with ionic and other terrible frameworks is, that they make you use all of their stuff, it's so easy to write <ion-slides> instead of implementing swiper yourself (10 sec vs say 2min), but ultimately you regret it because you -will- drop it in the end once you need performance, and then you spend a week for getting rid of it all and fixing the CSS issues.

Since I dropped ionic my fps for scrolling and other actions went through the roof.

Also, I no longer have everything encapsulated in a separate div, rather than the body. On many mobile browsers this causes a loss of screen space, as for example on samsung devices, the default browser comes with a tabbed navigation at the bottom screen that will not disappear if body doesn't scroll. Everything ionic does is make your life painful.

Honestly think about what you really need. Performance and small file sizes.

What you don't need: Bad documentation, breaking updates, terrible support and terrible reaction time by the team. And then on top, it will become difficult to find out what's causing the bug.. is it ionic or Angular? Most of the time it was ionic. But you loose that time on top.

Never. again.

:))

7 Comments

@Ben,

right, regarding the 404, I just match-all ** and redirect to index.html. Works nice on S3/Cloudfront, haven't tried on github yet.. this way, for development, you at least won't get an annoying 404..

7 Comments

I'm struggling to see the need for the Directive.
I have implemented something similar, listen to the ActivatedRoute fragment's, then do a document.querySelector, and scroll to the element if it exists. Don't see the need to have a directive wrapping every [id] element

15,848 Comments

@Brian,

If you want to add the code to your application, then I agree - you don't need to the directive. My intention was to be able to provide something that was completely transparent (ie, only added as module, but is not actually added to - for example - your root component's logic).

That said, I do think the directive approach does have an added bonus: being able to link to an element that is not on the page the moment after the route changes. Imagine that you are linking to a view that is data-driven and the data takes some time to load and therefore your [id] target is not on the page immediately. You could work that logic into your post-fragment-change logic, no doubt; but, by inverting the relationship, you can allow the anchor to "scroll to itself" when the content loads.

Both ways have pros and cons, I am sure.

15,848 Comments

@Mark,

That's super interesting about the tabbed interface not disappearing unless the body scrolls - I had never considered that, but that would be super annoying for sure.

I guess part of what I always thought Ionic was for was to help with "native" app development (ie, packaging your web-app inside a WebView inside a native app wrapper). Maybe that's cause Cordova / PhoneGap and Ionic were often used together?

Right now, I'm hoping to start looking at "Progressive Web Apps", which I think overlaps a lot with the whole PhoneGap concept. That said, I'm on iOS, and only Safari can "save to the home screen" and Safari doesn't support Progressive Web Apps yet ... meh, Apple!

1 Comments

Thanks for the code and post Ben, just wanted to point out that I had to inject this directly into my lazy-loaded child module to get it to work in that module - it wouldn't work if I just injected it into the root module.

15,848 Comments

@Ben,

Very interesting! I have to admit I have no experience or insights when it comes to lazy-loaded modules. Do you have a sense of why you had to inject it directly into the lazy-loaded components? Do they not inherit from the root dependency-injection container? I know that a lazy-loaded module has it _own_ DI container that is below the root one. But, I don't have a good sense of how they all interact.

1 Comments

Thank you very much "Mr. BEN NADEL". The post is very useful, i have been thinking of a way to do it until i taught of researching on google which i actually did and found your website link with the exact and best solution to my problem. You are really good. thumbs up! and keep it up.

Thanks to others that commented as well, your comments are very useful too.

1 Comments

Gave it a try but seemed to have no luck (@Mark's suggestions came in useful).
However apparently the Angular may finally be applying their wisdom to bring the webs oldest native interaction to the modern web. I to am baffled by it's omittance given it's even widely used in their docs.
https://github.com/angular/angular/pull/20030

8 Comments

@Ben @Mark,

Great piece of code, however it works the very first time you click on an anchor link. The second time it got to the choopppperrrrr! I mean, it doesn't work at all.
Even if I go to another page, so the url changes and the anchor part disappear, go back to the same page and click again, it doesn't work.

However, in your example it works always, so not sure what's wrong with my implementation. First click always work, second click never, I'm frustrating.

Thank you in advance,
Antonio

8 Comments

I thinks the issue is due to the piece of code with the rice condition comment. The one with the timeout. If I try a few combinations, it works, it doesn't and so...
Did you know what's wrong with this piece of code?

Thank you in advance
Antonio

3 Comments

It's quite similar to the solution I tried (without the polyfill approach) and it has the same issue for me : When I load the page, it doesn't work because, from what I suspect, the DOM element exists, so the scrollIntoView does not fail, but the contents are not loaded yet so it scrolls at a position of 0 and then the images and all other ressources are loaded but the scroll already happened and I'm at the top of my page.

Once the page is loaded, it works well though.

3 Comments

To be precise, I've got an issue only in Chrome. This works on first load with firefox and edge.

I've noticed that the scroll happens during a microsecond before getting sent to the top for some reason. Probably linked to Chrome.

3 Comments

Hey, I just overcame the issue with Chrome, it was quite simple in the end.

Just add this code at the beginning of the ngOnInit in the FragmentTargetDirective :

if ('scrollRestoration' in history) {
    history.scrollRestoration = 'manual';
}

It disables the Chrome automatic scroll positioning and lets you scroll yourself whenever there is an anchor.

15,848 Comments

@Vbourdeix,

Reading this:

.... from what I suspect, the DOM element exists, so the scrollIntoView does not fail, but the contents are not loaded yet so it scrolls at a position of 0

I am wondering if you could also approach this by hiding some layout elements until the content is loaded. Something like (just making this up):

<div *ngIf="isLoaded">
	....
	<div id="some-scroll-target> ... </div>
	....
</div>

This way, you don't even show the some-scroll-target until the content is loaded. This will delay the linking of the directive until you know the layout will be somewhat more accurate.

15,848 Comments

@Antuan,

Unfortunately, I couldn't find another way around the race-condition. But, the race-condition was about fixing the first call, not the subsequent calls. So, if it's the second call that breaks, that's confusing. Maybe you could try adding a console.log() statement to the .fragment.subscribe() callback, see if it's actually getting called?

15,848 Comments

@Paul,

I suspect that scroll retention and restoration is a surprisingly hard topic. From my experience, even getting the "push state" and "pop state" to behave consistently seems ... baffling. I tried to create a polyfill for that feature as well, and it is significantly more complicated that this [comparatively] simple fragment polyfill.

I look forward to seeing what the Angular team comes up with #FingersCrossed

1 Comments

Thanks a lot for the insight!

I face an error ' Can't resolve all parameters for createRouterScroller: ([object Object], ?, [object Object]).'

If someone can help here. I followed exactly your tutorial, Ben.

15,848 Comments

@Iris,

Hmm, that's an odd error. Since this blog post, Angular has had new releases that include similar types of behavior (I believe, maybe). It's possible that something is conflicting. I know that they now have a "Retain Scroll" kind of behavior. It's possible that it is conflicting somehow (though I am not sure how that would be).

You also might want to check that you're including all the right Router modules. Because the error indicates that something is looking for a Service Type that is not being provided, it's possible you are missing a Router module that provides something?

3 Comments

great job ben, you saved my time :)

FragmentTargetDirective is Works like a charm.

am getting the below issue plz help me to resolve this or is it expected behavior ?

second time clicking on the same link (after changing the scroll position) its not working.

Link : http://bennadel.github.io/JavaScript-Demos/demos/router-jump-to-fragment-angular5/#/app

below is the steps on your sample application

steps :

  1. click on B View #bottom link
  2. now scroll up manually
  3. again click on click on B View #bottom link
    observe its not scrolling to the element location

how we can resolve this issue, i tried several ways but couldn't succeeded.

15,848 Comments

@James,

That's a good question. The problem is that the fragment portion of the URL isn't actually changing when you click on the anchor link for the second time. As such, I don't think a new fragment event is triggered (which is what the anchor directive is listening for).

One thing that we could possibly do is subscribe to the NavigationStart and NavigationEnd events. And, in the NavigationStart event, if the event.url is the same as the "current URL", we could scroll back to the anchor target in the NavigationEnd event.

I could try to play with that idea.

3 Comments

@Ben,

i try to follow your approach, subscribed router events on FragmentTargetDirective but none of the events are fired when second click on the same element. ;)

8 Comments

Just for your information, when I posted my question on this thread I was developing this store in Angular: https://eltocadordecarlota.com
On product pages I show a few bullets and the typical read "more link", and I needed the jump-to-anchor to move the user to the more information section when clicking on that link. For example, on this page: https://eltocadordecarlota.com/productos/secador-de-pelo-philips-hp8233-thermoprotect-ionic-2200w the link "Ver más detalles".
The implementation of this article worked like a charm, except that it works the very first time. Second time the user click on the same link, it does not work.
Well, for the 99% of the cases it's ok and I assume that bug.

15,848 Comments

@James,

Yeah, I see what you're saying. It looks like the Router navigation events don't fire if you're clicking on the same fragment again. Ok, let me noodle on this a a bit .....

15,848 Comments

@All,

Making some progress on the fragment stuff. First, I'm going to just get it working on top of the Angular 6+ native fragment behavior. Then, once I get that working, I'll move backwards into the more robust polyfill. This morning, I dug into the way Angular consumed host bindings:

www.bennadel.com/blog/3536-host-bindings-don-t-prevent-default-event-behavior-until-after-all-event-handlers-have-executed-in-angular-7-1-1.htm

... which is important because its the routerLink directive that I need to tap into. So, my first plan is to create a sibling directive for routerLink that polyfills the fragment behavior for when the given fragment is already in the URL.

More to come soon.

8 Comments

btw, website El tocador de Carlota is on top of the Angular 7, not sure if I had to update anything but I think your code work as it on top of Angular 7 (except for the click twice issue)

15,848 Comments

@Antuan,

To be honest, I haven't tried it in the newer Angular yet, only Angular 5. I think in Angular 6, the new behavior was still defaulted to "off". Not sure, off-hand, if it is defaulted "on" in Angular 7.

I'll find out more in the near future.

15,848 Comments

@All,

If you are already using the native anchorScrolling: "enabled" in Angular 7, I've created a small polyfill that will enable second-clicks of the fragment routerLink:

www.bennadel.com/blog/3537-polyfilling-the-second-click-of-a-routerlink-fragment-in-angular-7-1-1.htm

This polyfill is a Directive that selects on a[routerLink][fragment] and polyfill only the edge-case in which the fragment is clicked a Nth time. Hopefully this helps newer apps before the Angular team has fixed this bug.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel