A Single Route Parameter Can Match Multiple URL Segments In Angular 4.4.4
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:

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:
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.