Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Luis Majano
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Luis Majano@lmajano )

Chaining Absolute And Local Redirects With The Router In Angular 7.2.13

By Ben Nadel on

One of the nice features of the Angular Router is that it can perform a "local" redirect at any level of the Route configuration. It can even chain local redirects together as it steps down through the configuration tree. A local redirect will even carry-forward the route segments that come after redirected segment. But, for design reasons that I don't fully understand, none of this works after you perform an "absolute" redirect. In other words, if your route configuration performs an absolute redirect, then no local redirects in the subsequent route will be honored. We can get around this limitation by performing the absolute redirect with a Component rather than a "redirectTo" property 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 understand the need to chain "absolute" and "local" redirects, imagine this scenario. Your application defines a feature that allows an administrator to manage a list of users. This feature is defined at the route:

/users

Then, later on in the application development life-cycle, the powers-that-be decide to rename the feature from "User Management" to "People Management"; and, as such, change the route from "/users" to:

/people

Of course, they don't want to break links that may have been bookmarked out in the wild; so, they define a redirect within the Angular Router configuration to forward the old "/users" route to the new "/people" route:

  • {
  • path: "users",
  • redirectTo: "/people"
  • }

At this point, everything works exactly as you would hope.

Of course, all things change. And, eventually, the "people" feature gets re-engineered to use a list page. Unaware that there's a redirect from the old "/users" route to the "/people" route, the developers come in and add a local redirect in the "People Management" feature such that forwards the user from the feature-root to a "list" page:

  • {
  • path: "people",
  • children: [
  • // NOTE: Local redirect from (/people) to (/people/list).
  • {
  • path: "",
  • pathMatch: "full",
  • redirectTo: "list"
  • },
  • {
  • path: "list",
  • component: PeopleListComponent
  • }
  • ]
  • }

From the feature's perspective, everything works perfectly well. But, unknowingly, these developers just broke the old redirect from the "/users" route. This is because the Angular Router won't follow the local "list" redirect after it has followed the absolute "/people" redirect. As such, any bookmarked links to the old "/users" feature will leave the Angular app in an incomplete state (possibly rendering a blank page or half-formed view).

To get around this limitation, we can use a transient Component - rather than the "redirectTo" property - to perform the redirect operation. Essentially, we're going to create an Angular Component that does nothing but invoke the Router.navigateByUrl() method. It turns out, if you use the Router to navigate to an absolute URL, any local redirects at the destination URL are honored and work as expected:

  • // 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 METHOD.
  • // ---
  •  
  • // 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 );
  •  
  • }
  •  
  • }

As you can see, all that this AbsoluteRedirectComponent does it look for the "data.redirectTo" property and then pass it to the Router.navigateByUrl() method. I'm doing this in the ngOnInit() method, rather than the constructor(), because it allows the previous navigation to finish before starting the new navigation (to the data.redirectTo URL). This feels more in alignment with the in-built "redirectTo" property, which doesn't emit a "cancel" event.

It should be noted that using the Router.navigateByUrl() method will "break" the Back Button. Unlike the in-built "redirectTo" property, which doesn't push the intermediary location onto the History, the AbsoluteRedirectComponent will allow its own route to be pushed onto the History before forwarding the user. This makes it difficult for the user to use the browser's back button to navigate to the pre-redirect Location. Because of this, you might want to considering using this approach only for application ingress routes.

To this strategy in action, let's define an Angular Router configuration with our "people" and "users" routes as well as a fun little route that bounces the user to various absolute locations before finally taking them to the "people" route:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { AbsoluteRedirectComponent } from "./absolute-redirect.component";
  • import { PeopleListComponent } from "./people-list.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • BrowserModule,
  • 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
  • }
  • )
  • ],
  • providers: [
  • // CAUTION: We don't need to specify the LocationStrategy because we are setting
  • // the "useHash" property in the Router module above (which will be setting the
  • // strategy for us).
  • // --
  • // {
  • // provide: LocationStrategy,
  • // useClass: HashLocationStrategy
  • // }
  • ],
  • declarations: [
  • AbsoluteRedirectComponent,
  • AppComponent,
  • PeopleListComponent
  • ],
  • bootstrap: [
  • AppComponent
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

In this case, we're expecting the in-built redirect from "/users" to "/people" to break for all the reasons above. But, by using the AbsoluteRedirectComponent, we're expecting the redirect from "/ping" to eventually get the user to "/people/list".

To test this, I've created a very simple App Component template:

  • <p>
  • <a routerLink="/">Home</a>
  • &mdash;
  • <a routerLink="./people">People</a>
  • &mdash;
  • <a routerLink="./users">Users (old "People" route)</a>
  • &mdash;
  • <a routerLink="./ping">Ping-Pong-People</a>
  • </p>
  •  
  • <router-outlet></router-outlet>

And, if we load this Angular app in the browser and attempt to navigate to "/ping", we get the following output:


 
 
 

 
 Chaining absolute and local redirects with the Router in Angular 7.2.13. 
 
 
 

As you can see, even through we chained several absolute URL redirects together, we were still able to follow the local redirection from "people" to "people/list".

Once you are in an Angular application, local redirects work in the vast majority of Router use-cases. But sometimes, such as when trying to honor old URLs, it's necessary to chain a local redirect after an absolute redirect. Out of the box, the Angular Router doesn't allow this. But, as you can see in my demo, we can side-step this limitation by performing the redirect with a transient component rather than the Router configuration in Angular 7.2.13.



Reader Comments

@All,

It just occurred to me that I might be able to use a "replace state" navigation extra to get around the "Back Button" issue. I will follow-up on that after work.

Reply to this Comment

I was correct. You can use the replaceUrl Navigation Extras option in order to get around the broken Back Button behavior. Or, in other words, you can use replaceUrl to create a more natural and intuitive back button experience:

https://www.bennadel.com/blog/3602-using-replaceurl-in-order-to-honor-the-back-button-while-chaining-absolute-redirects-in-angular-7-2-13.htm

... in this follow-up post, I am updating the .navigateByUrl() call as such:

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

}

I believe that this update makes a much more viable solution.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.