Using "replaceUrl" In Order To Honor The Back-Button While Chaining Absolute Redirects In Angular 7.2.13
Over the weekend, I took a look at using a transient component to side-step the limitations of the Router, allowing absolute and local redirects to be chained in Angular 7.2.13. In that post, I stated that the downside of using a transient component for the redirect was the router.navigateByUrl() method broke the Browser's natural "Back Button" behavior. Which is true, by default. However, we can easily honor the Browser's Back Button behavior by adding the "replaceUrl" property to the Navigation Extras of our router.navigateByUrl() call. As such, I wanted to put together a super quick follow-up post in Angular 7.2.13.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
To quickly recap my previous post, the Angular Router doesn't allow local redirects to be performed after absolute redirects. This is "by design"; and, something that I can't really explain. So, to get around this Router limitation, I created a transient component that allows the "redirectTo" functionality to be moved out of the native Router configuration and into a Router.navigateByUrl() method call.
To demonstrate this, I created a Router configuration that would bounce the user across multiple absolute redirects followed by a local redirect:
/ping --> /pong --> /people --> ./list
Here is the relevant Router configuration:
RouterModule.forRoot(
[
{
path: "people",
children: [
// NOTE: Local redirect from (/people) to (/people/list).
{
path: "",
pathMatch: "full",
redirectTo: "list"
},
{
path: "list",
component: PeopleListComponent
}
]
},
// SCENARIO: We have an old route for "/users" that we now want to
// redirect to the "/people" route so that old links continue to work.
// Only, this WILL BREAK because the "/people" route has a local redirect
// to the "list" end-point. This WILL NOT BE HONORED as local redirects
// are never followed after an absolute redirect.
{
path: "users",
redirectTo: "/people" // <---- WILL NOT WORK (like we want it to).
},
// To get AROUND the ABSOLUTE REDIRECT limitation, we can use a transient
// component. While the in-built "redirectTo" command won't allow for
// subsequent local redirects, the Router.navigateByUrl() method will.
// Meaning, an absolute URL navigation can be followed by local redirects
// in the Router configuration. To demonstrate, we'll chain a few
// absolute redirects that will eventually consume the local "people"
// redirect (to people/list).
{
path: "ping",
component: AbsoluteRedirectComponent,
data: {
redirectTo: "/pong" // <--- absolute redirect to FOLLOWING route.
}
},
{
path: "pong",
component: AbsoluteRedirectComponent,
data: {
redirectTo: "/people" // <--- absolute redirect to PEOPLE route.
}
}
],
{
// Tell the router to use the hash instead of HTML5 pushstate.
useHash: true,
// Enable the Angular 6+ router features for scrolling and anchors.
scrollPositionRestoration: "enabled",
anchorScrolling: "enabled",
enableTracing: false
}
)
As you can see, in the "ping" and "pong" routes, I have a "redirectTo" property; but, it's not part of the native configuration. Instead, it's in the "data" property where it will be consumed by the AbsoluteRedirectComponent.
Now, in my previous post, the AbsoluteRedirectComponent executed the Router.navigateByUrl() method as follows:
public ngOnInit() : void {
console.warn( "Absolute redirect to:", this.redirectTo );
// NOTE: We could have performed the .navigateByUrl() in the constructor.
// However, doing so would have emitted a "navigation canceled" event. By waiting
// until the init method, we allow the previous navigation to complete before we
// start the new navigation. This feel more in alignment with the way the built-
// in "redirectTo" property works.
this.router.navigateByUrl( this.redirectTo );
}
This brought the user to the correct URL; however, if the user tried to navigate "Back" through the browser's history, they would have been immediately forced forward by the redirect.
To get around this, we can tell the Router to "replace the URL" during that navigation. This will replace the current state in the Browser's History API rather than pushing a new state onto the queue:
public ngOnInit() : void {
console.warn( "Absolute redirect to:", this.redirectTo );
// NOTE: We could have performed the .navigateByUrl() in the constructor.
// However, doing so would have emitted a "navigation canceled" event. By waiting
// until the init method, we allow the previous navigation to complete before we
// start the new navigation. This feel more in alignment with the way the built-
// in "redirectTo" property works.
this.router.navigateByUrl(
this.redirectTo,
// By replacing the current URL in the history, we keep the Browser's Back
// Button behavior in tact. This will allow the user to easily navigate back
// to the previous URL without getting caught in a redirect.
{
replaceUrl: true
}
);
}
With this change in place, if we open up the Angular application in the browser and then invoke the "ping-pong-people" link, we get the following output:
As you can see, with the "replaceUrl" property added to the Navigation Extra option of our Router.navigatebyUrl() method call, we don't persist the intermediary redirects to the Browser's history. This will provide a more intuitive Back Button experience for the user.
For completeness, here's the full AbsoluteRedirectComponent code:
// Import the core angular services.
import { ActivatedRoute } from "@angular/router";
import { Component } from "@angular/core";
import { Router } from "@angular/router";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-absolute-redirect",
template:
`
<em>Redirecting to <code>{{ redirectTo }}</code></em>
`
})
export class AbsoluteRedirectComponent {
public redirectTo: string;
private router: Router;
// I initialize the absolute-redirect component.
constructor(
activatedRoute: ActivatedRoute,
router: Router
) {
this.router = router;
this.redirectTo = activatedRoute.snapshot.data.redirectTo;
}
// ---
// PUBLIC METHODS.
// ---
// I get called after the inputs have been found for the first time.
public ngOnInit() : void {
console.warn( "Absolute redirect to:", this.redirectTo );
// NOTE: We could have performed the .navigateByUrl() in the constructor.
// However, doing so would have emitted a "navigation canceled" event. By waiting
// until the init method, we allow the previous navigation to complete before we
// start the new navigation. This feel more in alignment with the way the built-
// in "redirectTo" property works.
this.router.navigateByUrl(
this.redirectTo,
// By replacing the current URL in the history, we keep the Browser's Back
// Button behavior in tact. This will allow the user to easily navigate back
// to the previous URL without getting caught in a redirect.
{
replaceUrl: true
}
);
}
}
With this new configuration, the AbsoluteRedirectComponent gives us the ability to chain both absolute and local redirects while still honoring the browser's "natural" Back Button experience. This is exactly what we need in order to maintain old application links and other types of internal route shortcuts and aliases in Angular 7.2.13.
Want to use code from this post? Check out the license.
Reader Comments
Hey, I think I asked this to you before and it was a long time ago: what tool do you use to create your mockups? I love the font and the arrows.
@Rob,
So, historically, I've used Adobe Fireworks (formerly Macromedia Fireworks) to create my illustrations. But, more and more, I've been trying to move to InVision Studio. You can tell which are which because my Studio-based illustrations have no bold text.
The font I use is called "Coming Soon" and is a free font: https://fonts.google.com/specimen/Coming+Soon
The problem is, it only has one weight, "Regular". This is why my Studio versions (like the one in this post) don't have any Bold text. Studio won't "fake" a bold font-face, like Adobe Fireworks will. I really should find a font-family with more style choices, so that I can more fully embrace Studio. But, I really like "Coming Soon" :D
@All,
Another good use-case for the
replaceUrl
option is to push search filtering into the URL without bloating the Browser's history:www.bennadel.com/blog/3614-using-replaceurl-to-persist-search-filters-in-the-url-without-messing-up-the-browser-history-in-angular-7-2-14.htm
This also serves to maintain an intuitive Back Button experience while still allowing the URL to drive some critical functionality.
Thanks for the very good blog.It is informative.
But when i use ''replaceUrl'' the angular events are going crazy.
I have 'onint' and 'ondestroy' events.When i use replce url, it is firing 'ondestroy' event after the second time page loads.
ngOnInit() {
console.log('onint');
id=this.route.snapshot.paramMap.get('id')
if (id == "test") {
console.log('navigating');
this.router.navigate(['/test'], { replaceUrl: true });
}else{
////Do logic
}
}
ngOnDestroy(){
console.log('destroy');
//unload page-data
}
For the above code o/p:
1.onint
2.navigating
3.onint
4.destroy
Then destroy is calling at the end.
Which is unloading my page data.
Note: If don't use replce url,not firing destroy
Please help me.Why destroy calling after the page refreshed or loaded.
@Maheswar,
That's really strange! I don't immediately understand why the
replaceUrl
setting would change any of this behavior since it should only be affecting the History stack of the app, not the actual functionality of the rendering.That said, are you redirecting from one route right back to the same route? Or is the
ngOnDestroy()
being defined in a higher-level component? Perhaps you have some settings in our Router where you destroy components on route-change even if you are using the same route? I think that's the "route reuse" settings. Are you doing something unexpected there?