Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Michael Offner-Streit and Tanja Stadelmann and Gert Franz and Pierre-Olivier Chassay and Paul Klinkenberg and Marcos Placona
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Michael Offner-Streit Tanja Stadelmann Gert Franz Pierre-Olivier Chassay Paul Klinkenberg Marcos Placona

Using A Wild Card Shortcuts Route To Hide The Internal Routing Implementation In Angular 7.2.5

By
Published in Comments (2)

The other day, I looked at how to use wild card routes to traverse arbitrarily nested data in an Angular 7.2.4 application. That approach allows more application state to be pushed into the URL; which, ultimately, make the application more sharable and more correctly aligned with a user's expectation of browser behavior. This got me thinking about other ways in which wild card routes enhance shareability. Which got me thinking about "shortcut" routes. A shortcut route is a route designed to encapsulate the internal implementation details of the application route configuration. This allows for consistent long-term linking and the ability to trigger various routes and workflows that cannot be known ahead of time.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

One of the benefits of a route-based Angular application is that views within the application can be deep-linked. That is, a user can save (bookmark) or share a URL that points to a specific view contained within the application. Sometimes, however, not all features are URL-driven. And, some URLs are identified using runtime data (such as entity-IDs), which don't make sense across different user-contexts. As such, there are use-cases in which it can be helpful to have a URL - or set of URLs - that do nothing but provide a proxy to other areas of the application:

Using wild card shortcut URLs in an Angular 7.2.5 application in order to provide consistent, long-term linking.

Consider the following use-cases:

  • There are often workflows and views within an application that are not strictly driven by the URL. Onboarding and Activation workflows are generally driven by user-state. Modal windows are often triggered by user-interaction. A shortcut URL can provide a way to trigger these views explicitly. This can be especially helpful when a user is corresponding with a Support Team and the Support Team needs to provide links to the user.

  • Many routes within an application contain embedded parameters that are not easily known ahead of time (such as user-IDs, account-IDs, and team-IDs). A shortcut URL can provide a static ingress to application routes that require runtime data to generate. Again, this can be very helpful for a Support Team's interaction with a user.

  • Even when an application route is known ahead of time, it can change over time. A shortcut URL can provide a more static and consistent long-term means of deep-linking into an application when the location of the link is maintained outside of the application, such as in Support Documentation and FAQ lists. This allows the internal routing implementation of the application to change without breaking external links.

Of course, shortcut URLs of this nature can be implemented without a wild card route - they can be explicitly identified in the router configuration of your Angular application. But, for the sake of this demo, we're going to use a wild card route. This means that we only have to include one hook in our primary router configuration. And then, differentiate between various shortcuts within a single View.

Regardless of how shortcut URLs are implemented, I would definitely recommend using a "View Component" to manage the shortcut. As opposed to trying to jam this functionality into a "Route Guard" or some other type of non-View workflow. By using a "View", you can provide meaningful feedback and error messages to the user if the shortcut is invalid or requires subsequent data-loading in order to fulfill.

That said, for the purposes of this demo, I'm going to put my shortcut URLs behind a "/go" URL-prefix. This way, the shortcuts don't collide with any other routes within the application. And, I'll be using the "**" wild card route to capture all URLs under said prefix:

// 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 { GoViewComponent } from "./go-view.component";
import { MockViewComponent } from "./mock-view.component";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	imports: [
		BrowserModule,
		RouterModule.forRoot(
			[
				// The "go" prefix is our ingress into the short-cut route. However,
				// since we want our short-cuts to be flexible, we're going to use a
				// wild-card sub-path. This way, we'll catch everything after "/go".
				{
					path: "go",
					children: [
						{
							path: "**",
							component: GoViewComponent
						}
					]
				},

				// Since we don't really have views in this demo, we'll just use a catch-
				// all route for all of our non-shortcut views.
				{
					path: "**",
					component: MockViewComponent
				}
			],
			{
				// 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: [
		AppComponent,
		GoViewComponent,
		MockViewComponent
	],
	bootstrap: [
		AppComponent
	]
})
export class AppModule {
	// ...
}

As you can see, in my Angular application's route configuration, all "/go/**" routes are being captured by the GoViewComponent. The GoViewComponent must then inspect the URL and redirect the user to the target destination.

If all of your shortcut URLs are simple static strings, then the implementation of the GoViewComponent is a breeze. But, in order to make this demo a bit more interesting, I wanted to account for cases in which the shortcut URL contains data-driven parameters (such as those that might be included in a Transactional Email with links back into the application).

To do this, my shortcuts are represented by a collection of "route" patterns that correspond to either a static "redirect" or a function that returns a dynamic string. If the "redirect" value is a Function, any embedded parameters contained within the route pattern will be extracted and passed as ordered-arguments to the Function.

The purpose of the Function-based redirect is to allow the redirect URL to incorporate both route-parameters and application-state when calculating the destination URL. Since the GoViewComponent can leverage dependency-injection, any Angular service can be injected into the GoViewComponent and used within the redirect function.

NOTE: My implementation doesn't allow for asynchronous data-loading; but, the "redirect" function could easily be updated to return a Promise or a static string.

The GoViewComponent will then iterate over the collection of shortcut routes and redirect the user to the first match.

// Import the core angular services.
import { ActivatedRoute } from "@angular/router";
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { Subscription } from "rxjs";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface ShortcutMatcher {
	route: string;
	redirect: string | RedirectFunction;
}

interface RedirectFunction {
	( a?: string, b?: string, c?: string, d?: string, e?: string ): string;
}

@Component({
	selector: "go-view",
	styleUrls: [ "./go-view.component.less" ],
	template:
	`
		<p *ngIf="isRedirecting" class="note">
			Redirecting....
		</p>

		<p *ngIf="( ! isRedirecting )" class="not-found">
			Sorry, we didn't recognize your short-cut.
			Trying <a routerLink="/">going back to the home-page</a>.
		</p>
	`
})
export class GoViewComponent {

	public isRedirecting: boolean;

	private activatedRoute: ActivatedRoute;
	private router: Router;
	private shortcutMatchers: ShortcutMatcher[];
	private urlSubscription: Subscription | null;

	// I initialize the go component.
	constructor(
		activatedRoute: ActivatedRoute,
		router: Router
		) {

		this.activatedRoute = activatedRoute;
		this.router = router;

		this.isRedirecting = true;
		this.urlSubscription = null;

		// The goal of the short-cut is to both decouple the external world from the
		// internal implementation of the routing system; and, to allow more complex
		// routes to be calculated using application state (which may not be knowable
		// by the external world). The short-cuts can be simple, static strings. Or,
		// they can be data-driven strings that contain embedded IDs.
		this.shortcutMatchers = [
			{
				route: "boards",
				redirect: "/projects;type=board"
			},
			{
				route: "favorites",
				redirect: "/projects/favorites"
			},
			{
				route: "most-recent",
				redirect: () => {

					// NOTE: The "15" here is some sort of app-state value.
					return( `/activity/${ 15 }/items` );

				}
			},
			{
				route: "profile",
				redirect: "/account/profile"
			},
			{
				route: "prototypes",
				redirect: "/projects;type=prototype"
			},
			{
				route: "comment/:conversationID/:commentID",
				redirect: ( conversationID, commentID ) => {

					return( `/inbox/threads/${ conversationID }/comment/${ commentID }` );

				}
			}
		];

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called once when the component is being destroyed.
	public ngOnDestroy() : void {

		( this.urlSubscription ) && this.urlSubscription.unsubscribe();

	}


	// I get called once after the component has been mounted.
	public ngOnInit() : void {

		this.urlSubscription = this.activatedRoute.url.subscribe(
			( urlSegments ) => {

				// With the wild card sink route, we can get the short-cut by collapsing
				// all of the UrlSegments down into a single string. This will allow our
				// short-cuts to contain arbitrary values.
				var shortcut = urlSegments
					.map(
						( urlSegment ) => {

							return( urlSegment.path );

						}
					)
					.join( "/" )
				;

				this.redirectUser( shortcut );

			}
		);

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I try to match the given shortcut against the given shortcut matcher. If a match
	// is made, the redirect URL is returned; otherwise, null is returned.
	private matches( shortcutMatcher: ShortcutMatcher, shortcut: string ) : string | null {

		var paramPattern = /:[^/]+/g;

		// Take the route definition, which may include embedded ":param" notation, and
		// convert it to a RegEx pattern that will match any param between slashes.
		var routePattern = new RegExp( ( "^" + shortcutMatcher.route.replace( paramPattern, "([^/]+)" ) + "$" ), "i" );

		// Check to see if the incoming shortcut can be matched by the route pattern.
		// --
		// NOTE: If it does, the CAPTURED GROUPS in the resultant array will represent
		// the named route-params that were embedded in our shortcut matcher.
		var routeMatches = shortcut.match( routePattern );

		// If the route pattern did not match, short-circuit the function.
		if ( ! routeMatches ) {

			return( null );

		}

		// At this point, we know that our route matched against the incoming shortcut.
		// Now, we need to figure out how to generate the next URL. If the redirect in
		// the matcher is a simple string, just return it - no processing is needed.
		if ( typeof( shortcutMatcher.redirect ) === "string" ) {

			return( shortcutMatcher.redirect );

		// ON THE OTHER HAND, if the redirect in the matcher is a Function, then we have
		// to try and extract any named parameters from the route pattern and pass those
		// as parameters to the redirect function.
		} else {

			// NOTE: The .matches() function returns the full match as the first element.
			// However, we only want to pass the CAPTURED GROUPS as the arguments to our
			// redirect function. Hence, the .slice().
			return( shortcutMatcher.redirect( ...routeMatches.slice( 1 ) ) );

		}

	}


	// I redirect the user to the given short-cut.
	private redirectUser( shortcut: string ) : void {

		console.warn( "Redirecting via short-cut:", shortcut );

		for ( var shortcutMatcher of this.shortcutMatchers ) {

			var nextUrl = this.matches( shortcutMatcher, shortcut );

			// If we found a match for the shortcut, redirect the user to the target URL
			// and stop searching for further matches.
			if ( nextUrl ) {

				this.router.navigateByUrl( nextUrl );
				return;

			}

		}

		// If we made it this far, none of our matchers were matched against the given
		// short-cut. As such, we are not able to redirect the user. And, in this demo,
		// such an outcome will leave the user on the short-cut view with the error
		// message.
		this.isRedirecting = false;

	}

}

As you can see, when the GoViewComponent is instantiated, it starts watching for changes in the "url" of the ActivatedRoute. This will grant us access to the URL-Segments that come after the "/go/" prefix. Once the segments are extracted, I then try to match them against my collection of shortcut routes; and, redirect the user to the first matching shortcut.

To test this, I set up a simple App Component that provides some links to our shortcut URLs:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<ul>
			<li><a routerLink="go/boards">GO: Boards</a></li>
			<li><a routerLink="go/favorites">GO: Favorites</a></li>
			<li><a routerLink="go/most-recent">GO: Most-Recent</a></li>
			<li><a routerLink="go/profile">GO: Profile</a></li>
			<li><a routerLink="go/prototypes">GO: Prototypes</a></li>
			<li><a routerLink="go/comment/111/222">GO: Comment/111/222</a></li>
			<li><a routerLink="go/foobar">GO: FooBar</a> (not valid)</li>
		</ul>

		<router-outlet></router-outlet>
	`
})
export class AppComponent {
	// ...
}

Now, if click on the link for:

"go/comment/111/222"

... we can see that our GoViewComponent captures the shortcut, extracts the two data-driven parameters, and redirects the user to:

"/inbox/threads/111/comment/222"

... giving us the following browser output:

Redirecting from a wild card shortcut URL to an internal application route in Angular 7.2.5.

Depending on your type of Angular application, the concept of shortcut URLs may seem like overkill. And, in many cases, they probably are. But, the nice thing about a set of shortcut URLs is that they can serve to hide the internal implementation details of your routing, providing a more consistent long-term link-target for external documentation and correspondence. If nothing else, I hope this sheds light on additional use-cases for wild card routes in an Angular 7.2.5 application.

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

Reader Comments

15,848 Comments

@All,

After this post, I was very unsettled by all the "Work" I was doing parsing the shortcut route using RegExp. After all, this is exactly the kind of heavy lifting that the Angular Router already does. So, I wanted to revisit this idea, but move more of the configuration into the Angular Router itself:

www.bennadel.com/blog/3575-creating-shortcuts-by-mapping-multiple-routes-on-to-the-same-view-component-in-angular-7-2-5.htm

The new approach feels much cleaner and easier to follow. I have to distribute the configuration a bit for the "complex routes"; but, to the benefit of much more straightforward code.

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