Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Richard Worth
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Richard Worth@rworth )

A Single Route Parameter Can Match Multiple URL Segments In Angular 4.4.4

By Ben Nadel on

UPDATE: If you are parforming a prefix-based, local-direct, you may want to know that the Router will automatically append the non-local URL segments to the redirectTo action, making prefix-based mappings dead simple in Angular 4.4.4.

When I was learning about route configuration in Angular 4, I automatically assumed that a route parameter, like ":id" or ":type", would only match against a single URL path segment (ie, the portion of a URL between two "/" delimiters). As is turns out, however, a route parameter is capable of matching any non-empty portion of the URL. This gives us the ability to redirect based on a URL-prefix, capturing and forwarding the entire URL-suffix using a single route parameter.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

First off, a huge thanks to Juri Strumpflohner who show me that Angular route parameters work in this way; I did not stumble upon this behavior. In fact, this post is little more than me documenting what he showed me was possible.

That said, to demonstrate the path-capturing capabilities of a route parameter, I've created a simple Angular demo in which we match any route that begins with the "/a" prefix and redirect the user to the "/b" prefix, keeping the rest of the URL in tact. So, for example, if you were to navigate to:

/a/foo/bar/baz

... you would be redirected to:

/b/foo/bar/baz

Notice that the "..../foo/bar/baz" portion of the URL is maintained while redirecting from "/a" to "/b".

Let's take a look at the route configuration that makes this possible:

  • // 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 { BComponent } from "./b.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • var routes: Routes = [
  • // We're going to be redirecting any route that begins with "/a" to a route that
  • // begins with "/b", keeping the rest of the route in tact. We can do with this
  • // route segments which can actually match more than one slash-delimited token. In
  • // this case, we're using ":restOfPath" to match the entire remainder of of the
  • // route - everything that follows the "/a/" prefix.
  • {
  • path: "a/:restOfPath",
  • redirectTo: "b/:restOfPath"
  • },
  • // Since the ":restOfPath" path parameter has to match a non-zero-length value, we
  • // have to include an additional redirect for the naked "/a" route (if we want to
  • // handle those as well).
  • {
  • path: "a",
  • redirectTo: "b"
  • },
  • {
  • path: "b",
  • children: [
  • // We're going to render all "/b"-prefix routes in the same component.
  • {
  • path: "**",
  • component: BComponent
  • }
  • ]
  • }
  • ];
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • bootstrap: [
  • AppComponent
  • ],
  • imports: [
  • BrowserModule,
  • RouterModule.forRoot(
  • routes,
  • {
  • // Tell the router to use the HashLocationStrategy.
  • useHash: true
  • }
  • )
  • ],
  • declarations: [
  • AppComponent,
  • BComponent
  • ],
  • 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 {
  • // ...
  • }

Notice that we are defining an "/a" path with a single route parameter:

path: a/:restOfPath

In this case, the :restOfPath parameter will match the entire URL suffix following the "a/" pattern. This may contain one URL segment; or, it may contain many URL segments. This route parameter can then be used to append the entire URL suffix to the redirect route:

redirectTo: b/:restOfPath

Since a route parameter can only match a non-empty value (as far as I know), we need to include a second redirect to handle the case in which the "/a" path is matched without any suffix at all. This way, we can redirect from a naked "/a" request to a naked "/b" resolution.

To play around with this route parameter redirection, I've created an app component that provides a variety of "/a"-prefixed links:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.css" ],
  • template:
  • `
  • <p>
  • Try going to one of these <code>/a</code> prefix routes
  • (which do not have materialized views):
  • </p>
  •  
  • <ul>
  • <li><a routerLink="/a">/a</a></li>
  • <li><a routerLink="/a/items">/a/items</a></li>
  • <li><a routerLink="/a/items/4">/a/items/4</a></li>
  • <li><a routerLink="/a/items/4/detail">/a/items/4/detail</a></li>
  • <li><a routerLink="/a/items/4/detail" fragment="anchor">/a/items/4/detail#anchor</a></li>
  • <li><a routerLink="/a/items/4/detail" [queryParams]="{ q: '1' }">/a/items/4/detail?q=1</a></li>
  • </ul>
  •  
  • <router-outlet></router-outlet>
  • `
  • })
  • export class AppComponent {
  • // ...
  • }

As you can see, I'm testing out several ways in which to construct an HREF value, including paths, query-string parameters, and anchors. All of these will be matched by our route configuration and redirected from the "/a" URL tree to the "/b" URL tree, which is rendered by the BComponent:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Event as NavigationEvent } from "@angular/router";
  • import { Router } from "@angular/router";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • styleUrls: [ "./b.component.css" ],
  • template:
  • `
  • <h3>
  • B-Prefix Component
  • </h3>
  •  
  • <p>
  • You have navigated to <code>{{ url }}</code>
  • </p>
  • `
  • })
  • export class BComponent {
  •  
  • public url: string;
  •  
  • // I initialize the B-view component.
  • constructor( router: Router ) {
  •  
  • // As the user navigates through the "/a"-prefix routes, they will all be
  • // redirected to the "/b"-prefix routes that are rendered by this component. As
  • // that happens, this component will persist since we never navigate away from
  • // it. As such, we have to listen for navigation events to know when to update
  • // the view.
  • // --
  • // NOTE: We could have also listened to the ActivatedRoute.
  • router.events.subscribe(
  • ( event: NavigationEvent ) : void => {
  •  
  • this.url = router.url;
  •  
  • }
  • );
  •  
  • }
  •  
  • }

Here's we're just listening for navigation events and then echoing the current URL in the BComponent view. And, when we run this Angular application and click on one of the "/a" prefix links, we get the following output:


 
 
 

 
 Rotue parameters can match multiple url path segments, allowing for prefix-based redirects in Angular 4. 
 
 
 

As you can see, when we click on any of the URLs that start with the "/a" prefix, we are redirected to the "/b" prefix; and, the rest of the URL is passed along in the redirect, thanks to the :restOfPath route parameter.

When you start out by extracting entity IDs from URLs using route parameters like ":id" or ":userID", it's easy to believe that route parameters can only match a single segment in the URL path. But, in Angular, a route parameter can match multiple URL path segments. This allows us to capture entire URL suffixes and even forward those suffix values as part of Router redirect operations.




Reader Comments

@All,

It turns out, if you are doing a LOCAL-only redirect, this becomes an even easier problem - you just need to redirect the prefix, and the rest of the non-LOCAL URL segments are automatically appended to the redirectTo action:

https://www.bennadel.com/blog/3348-local-redirects-automatically-append-the-non-local-route-segments-in-angular-4-4-4.htm

That said, if you are doing an absolute redirect (starting with "/"), then you would still need this route-parameter approach.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.