Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Katie Maher
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Katie Maher

Creating Shortcuts By Mapping Multiple Routes On To The Same View-Component In Angular 7.2.5

By on

Earlier this week, I took a look at using wild card routes to create shortcuts in an Angular 7.2.5 application. That first approach worked; but, it required me to manually parse some of the more complex shortcuts using Regular Expression. Now, don't get me wrong - I freakin' love Regular Expressions; but, considering that I have this big beautiful Angular Router at my disposal, it struck me as silly to be doing so much manual labor. As such, I wanted to briefly revisit the concept of shortcuts and move more of the heavy-lifting into the Angular Router configuration by mapping multiple routes onto the same view-component in Angular 7.2.5.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

In my previous post, I was mapping all "go" route variations onto a single view-component using a single wild card child-route configuration. Essentially, I was mapping "/go/**" onto my GoViewComponent. This setup was nice because it put all the configuration in one place; but, it required me to manually parse any embedded parameters that appeared within the wild card portion of the route.

This kind of parsing is already handled by the Angular Router. We just have to move the patterns into the Route configuration so that Angular knows to parse the route segments for us. To do this, I'm going to define multiple child routes off of "/go/", each of which maps to the same GoViewComponent:

// 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 shortcut route. The goal of
				// the shortcut 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 shortcuts can be simple, static
				// strings. Or, they can be data-driven routes that contain embedded
				// IDs. For routes-with-parameters, we're going to leverage the existing
				// power of the Angular Router to manage the parsing.
				{
					path: "go",
					children: [
						// NOTE: All of these child routes use the SAME VIEW COMPONENT.
						{
							path: "comment/:conversationID/:commentID",
							component: GoViewComponent,
							data: {
								shortcut: "inbox"
							}
						},
						{
							path: "most-recent/:count",
							component: GoViewComponent
						},
						// If no shortcut matches, we still want to render the View
						// component so that we can render an error message for the user.
						{
							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 route configuration, I have three different child-routes under "/go", all of which map to the same GoViewComponent. This keeps my redirect logic in the GoViewComponent code; but, gets the Angular Router to parse the routes that contain embedded parameters. This way, in my GoViewComponent logic, I get to consume the "params" object on the ActivatedRoute without having to do any work!

You may have noticed that the first route also provides a "data" object with an explicit shortcut name. This gives the route configuration a chance to map the evaluated route onto a shortcut in cases in which the route-segments don't naturally map onto a meaningful name.

Now, in my GoViewComponent, all I have to do is look at the extracted shortcut and the implicitly parsed params object and perform the redirect using the Router instance:

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

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

@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 shortcut.
			Trying <a routerLink="/">going back to the home-page</a>.
		</p>
	`
})
export class GoViewComponent implements OnInit, OnDestroy {

	public isRedirecting: boolean;

	private activatedRoute: ActivatedRoute;
	private dataSubscription: Subscription | null;
	private router: Router;
	private userID: number;

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

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

		this.dataSubscription = null;
		this.isRedirecting = true;

		// NOTE: This value is just here to simulate some sort of "application state"
		// that would not be known ahead of time.
		this.userID = 15;

	}

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

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

		( this.dataSubscription ) && this.dataSubscription.unsubscribe();

	}


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

		this.dataSubscription = this.activatedRoute.data.subscribe(
			( data ) => {

				this.isRedirecting = true;

				// Since we're asking the Angular Router do the hard work of parsing some
				// of the parameterized routes, we can get the params from the snapshot
				// after each data-event.
				var params = this.activatedRoute.snapshot.params;

				switch ( this.getShortcut() ) {
					case "boards":
						this.router.navigateByUrl( "/projects;type=board" );
					break;
					case "favorites":
						this.router.navigateByUrl( "/projects/favorites" );
					break;
					case "inbox":
						this.router.navigateByUrl( `/inbox/threads/${ params.conversationID }/comment/${ params.commentID }` );
					break;
					case "most-recent":
						this.router.navigateByUrl( `/activity/${ this.userID }/items/${ params.count }` );
					break;
					case "profile":
						this.router.navigateByUrl( "/account/profile" );
					break;
					case "prototypes":
						this.router.navigateByUrl( "/projects;type=prototype" );
					break;
					default:
						// If we made it this far, the data doesn't represent a supported
						// shortcut. This is because the user got here based on the "**"
						// wild card route. 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;
					break;
				}

			}
		);

	}

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

	// I get the shortcut from the activated route.
	private getShortcut() : string {

		// For routes that require special parsing (ie, routes that contain params), the
		// shortcut will be defined in the data object. For all other routes, we'll just
		// join the URL segments together to formulate the shortcut.
		var snapshot = this.activatedRoute.snapshot;

		if ( snapshot.data.shortcut ) {

			return( snapshot.data.shortcut );

		}

		var hasParams = !! Object.keys( snapshot.params ).length;

		// Since the shortcut wasn't provided in the data, let's get it from the URL.
		var shortcut = snapshot.url
			.map(
				( urlSegment ) => {

					return( urlSegment.path );

				}
			)
			.filter(
				( path, index ) => {

					// If the URL contains parameters, then we only want to use the URL
					// segment in the first location (as subsequent location will contain
					// params). However, if there are no params, then use all segments.
					return( ( index === 0 ) || ! hasParams );

				}
			)
			.join( "/" )
		;

		return( shortcut );

	}

}

In this version, I am still performing a little bit of logic to figure out what the actual "shortcut" is. But, this logic is far more straightforward when compared to the RegExp-based parsing logic in my last approach. And, as you can see, in order to access the route params in my more complex shortcuts, all I have to do is grab the params object out of the Angular Router snapshot.

Now, if we run this version of the application and click on the "comment" shortcut, we get the following output:

Mapping multiple routes onto the same view-component in an Angular route.

As you can see, since were mapping the route, "comment/:conversationID/:commentID" onto the GoViewComponent, the Angular Router did all the parsing for us. Then, within the GoViewComponent logic, I was able to access the parsed parameters using the ActivatedRoute snapshot, wherein I then redirected the user to the desired target URL.

In my first approach, I appreciated that all of the configuration was in a single place. I liked the uniformity of that. But, it was uniformity at the cost of simplicity. With this new approach, the shortcut configuration is a little more distributed for the complex routes. But, it keeps the code much more straightforward and easy to maintain.

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

Reader Comments

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