Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jake Morehouse
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jake Morehouse ( @JacobMorehouse5 )

Closing Secondary Router-Outlet Views From Within The Named-Route View Components In Angular 4.4.4

By on

One of the cool features of the Router in Angular 4.4.4 is that it allows you to have multiple secondary - or named - routes that live alongside your primary router-outlet elements. These named routes are controlled by different portions of the browser URL and can render auxiliary Views within a given layout. Secondary routes represent a powerful but complicated feature; and, opening and closing a secondary route isn't exactly straightforward. In fact, it took me quite a bit of trial-and-error to figure out how to close a secondary router-outlet from within the named-route view component (ie, creating a self-closing secondary route). As such, I wanted to share my solution in case others are getting stumped.

Run this demo in my JavaScript Demos project on GitHub.

To demonstrate the self-closing functionality of a secondary route in Angular 4.4.4, I duplicated my named-outlet demo from the other day and added some "close" links to Chat widget. As a refresher on what the demo looks like, I have two primary layouts and a secondary "chat" outlet, which you can see in the app module's route configuration:

// 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 { ChatComponent } from "./chat.component";
import { LayoutAComponent } from "./layout-a.component";
import { LayoutBComponent } from "./layout-b.component";
import { LayoutWrapperComponent } from "./layout-wrapper.component";

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

var routes: Routes = [
	{
		// CAUTION: In order for the NAMED OUTLET child route to work (chat),
		// its parent segment must contain a non-empty path. As such, we're
		// using "main" for this wrapper component in order to ensure a non-
		// empty path segment.
		path: "main",
		component: LayoutWrapperComponent,
		children: [
			{
				path: "layout-a",
				component: LayoutAComponent
			},
			{
				path: "layout-b",
				component: LayoutBComponent
			},
			{
				outlet: "chat", // <--- Named outlet.
				path: "open",
				component: ChatComponent
			}
		]
	}

	// NOTE: We are OMITTING any not-found / catch-all REDIRECT as including it will
	// partially mask the fact that some of our code is broken. Essentially, the redirect
	// hides the broken URLs that get created.
	// {
	// path: "**",
	// redirectTo: "/main/layout-a"
	// }
];

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

@NgModule({
	bootstrap: [
		AppComponent
	],
	imports: [
		BrowserModule,
		RouterModule.forRoot(
			routes,
			{
				// Tell the router to use the HashLocationStrategy.
				useHash: true
			}
		)
	],
	declarations: [
		AppComponent,
		ChatComponent,
		LayoutAComponent,
		LayoutBComponent,
		LayoutWrapperComponent
	],
	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 {
	// ...
}

In this version of the demo, I am purposefully omitting any "**" (catch-all / sink) redirect as including it would partially mask the failure of some of the secondary route's self-closing approaches. As such, in order to really start the demo, you have to explicitly navigate to the "/main/layout-a" URL. To make that easier, I'm including a link to it in the app component.

Now, I'm not going to bother showing the app component. Nor am I going to bother showing you the wrapper component or the individual layout components because, frankly, they aren't all that relevant to the demonstration. All you have to know is that there is a secondary "chat" router-outlet that renders the following ChatComponent; and, the ChatComponent is going to attempt to close itself:

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

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

@Component({
	selector: "chat",
	styleUrls: [ "./chat.component.css" ],
	template:
	`
		<h3>
			Chat Widget
		</h3>

		<p>
			<em>How can I help you?</em>
		</p>

		<h4>
			Self-Closing Functionality
		</h4>

		<p>
			<a [routerLink]="[ '../', { outlets: { chat: null } } ]">Close (via RouterLink)</a>
			&mdash;
			<strong>Broken</strong>
			<em>(will appear to work, but will break on page-refresh)</em>.
		</p>

		<p>
			<a (click)="closeChatViaMe()">Close (via .navigate() using self)</a>
			&mdash;
			<strong>Broken</strong>
			<em>(will appear to work, but will break on page-refresh)</em>.
		</p>

		<p>
			<a (click)="closeChatViaParent()">Close (via .navigate() using parent)</a>
		</p>
	`
})
export class ChatComponent {

	private activatedRoute: ActivatedRoute;
	private router: Router;

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

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

	}

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

	// I attempt to close the chat by nullifying the secondary outlet using RELATIVE PATH
	// navigation commands.
	// --
	// CAUTION: This is BROKEN. This DOES NOT WORK.
	public closeChatViaMe() : void {

		this.router.navigate(
			[
				"../",
				{
					outlets: {
						chat: null
					}
				}
			],
			{
				relativeTo: this.activatedRoute
			}
		);

	}


	// I attempt to close the chat by nullifying the secondary outlet using the PARENT'S
	// ACTIVATED ROUTE.
	public closeChatViaParent() : void {

		this.router.navigate(
			[
				// NOTE: No relative-path navigation is required because we are accessing
				// the parent's "activatedRoute" instance. As such, this will be executed
				// as if we were doing this in the parent view component.
				{
					outlets: {
						chat: null
					}
				}
			],
			{
				relativeTo: this.activatedRoute.parent // <--- PARENT activated route.
			}
		);

	}

}

As you can see, there are three "close" links in the component that each attempt to close the secondary route (itself) in a different way. Of the three approaches, only the last one works. The first two break in the same manner; whether you're using the "../" relative navigation in the [routerLink]:

[routerLink]="[ '../', { outlets: { chat: null } } ]

... or, you're using the "../" relative navigation directly in the router.navigate() method:

this.router.navigate(
	[
		"../",
		{
			outlets: {
				chat: null
			}
		}
	],
	{
		relativeTo: this.activatedRoute
	}
);

... you'll be left with a page that is broken. At first, it may look like it works. And, in fact, if we open the secondary route for the Chat widget and then click either of the first two links, the Chat widget does, indeed, close. However, the browser URL is left in a corrupted state:

Self-closing secondary routes can be complicated in Angular 4.4.4.

As you can see, the Chat widget - the secondary route - visually closes; but, if you look at the browser URL, you can see that the nullified "chat" outlet is still present. At this point, if we were to refresh the browser, we would see the problem manifest itself:

Self-closing secondary routes may break in a way that is not obvious until refreshing the page.

The only way that I've been able to get this to work - to get a secondary route to close itself - is to execute the router.navigate() method relative-to the parent's ActivatedRoute instance:

this.router.navigate(
	[
		{
			outlets: {
				chat: null
			}
		}
	],
	{
		relativeTo: this.activatedRoute.parent // <--- PARENT activated route.
	}
);

In this case, we're omitting any relative navigation - "../" - because we're executing the .navigate() method relative to the parent component's ActivatedRoute instance. This will tell the parent component to close its local router-outlet for the "chat" widget, which works quite nicely.

It took me a bunch of trial-and-error to figure this out. And, I'm not sure if this is the "right" way to create a self-closing secondary route in Angular 4.4.4; but, it's the only way that I could get it done. Hopefully this helps anyone else who might be stumped by the self-closing problem.

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

Reader Comments

3 Comments

Hey Ben, thanks for the good read. I recently just completed my website (https://www.zadesigns.com) using Angular 4.4.6 and the entire thing was done with UI-Router. I have gotten 99% of everything I wanted to work using ui-router (lazy routes, aot, multiple named views, resolves, image preloading, etc etc) but the one thing I can't accomplish is a trigger wrapping my router-outlet and performing animation on both the in and out view using 1 trigger (something that you can do with the built in router but i can't get working with the ui-router). I made the decision to attempt to fork my website and redo it using angulars built in router. Man.. what a PITA. for starters, the mutliple named views being forced into the URL sucks. i just don't want my users seeing any type of component controller in a URL, and then giving them the ability to modify that url and get a different result. granted I have spent 2 days with angular router but I am experiencing the same issues you are writing about. In my work section I have 2 named views. with ui-router I can control both views with 1 url, but with the built in router i am forced to use a outlet in my url name (which i don't want) and i also have a terrible time managing that URL (like trying to close the drawer from the url or close the drawer from other states, etc). I think angular could solve alot of this stuff by having the URL inside some internal state manager and just expose a URL for each state, similar to what ui-router does. After 2 or 3 days of working with angulars router I am about to pull the plug and go back to ui-router and just come up with my own resolve / promise animation system to handle the queing of the views. wish i could use angulars built in query to do it, but i can't. i have a few stackblitz and a S.O. question out there but no bites so far. I'd recommend at least checking it out ,its pretty solid.

thanks!

15,688 Comments

@Zuriel,

I very much go back and forth with how I feel about what I want to put in the URL. For example, in my core Router exploration, I've decided to put "modal" windows in a named-route. This has some great benefits like being able to pass-in route-based parameters and to easily hook into the browser back-button. But, on the flip-side, I am not sure how I want to handle the browser refresh. Meaning, let's say I open an Error Modal window with named route:

modal:modals/error/not-found

Well, if the user refreshes the browser, that secondary route is _still_ in the URL. Which means that error modal shows up again, even through there was no programmatic triggering of an error.

Maybe I can solve that with some sort of route-guard. But I've sort of punted on that issue for the time being. Of course, this wouldn't be a problem if the URL didn't hold the modal window paths. But, I can't help but like the URL being the source of truth.

Obviously, still trying to figure out what it all means :D

15,688 Comments

@Ram,

Yeah, that should be possible. The parent view can subscribe to the events on the Router, and look for cases in which the subview is closed. However, if you want to refresh based on some state change, you may be better off just using some sort of shared-service that acts as event-emitter. For example, if the subview was an "edit" page, and the parent page was a "list" page, you could have some service emit an, "entityUpdated" event, which the list page could listen for and refresh it's data.

Or, if you use something like Redux or NgRx, you could probably emit some sort of "action" that the parent page is listening for in its reducers.

1 Comments

Hi,
when we route to the named outlet which is loaded already? I see it doesn't call resolve method or ngOnInit, since it is already loaded. What is the solution for running resolve methods and reload the named outlet when navigated again in the same page?

1 Comments

Thanks a ton! This so called "advanced" routing is unintuitive (or broken?!) as hell...

15,688 Comments

@Y,

I really love what they are trying to do. But, there are definitely a lot of hurdles and things to trip over. I think, ultimately, the biggest rule that I'm finding is that "modularity" or routes is just not an attainable outcome. Meaning, the idea of having a "feature module" that has no concept of where it is located within in an application (ie, doesn't know its route prefixes) and can only do relative navigation all over the place, is likely to be a overly-complex goal.

The more I embrace the idea that a given routable module needs to know where it is located, the easier my life has gotten.

15,688 Comments

@Harika,

I believe there is a setting for that - something about "component reuse". I think it may be the RouteReuseStrategy strategy in the Router configuration. But, I have to admit that I haven't played around with it -- I like to keep the route open, subscribe to changes in the route-params, and then re-fetch data as needed. It's just the approach I'm used to.

1 Comments

Thank you for this investigation. I am literally bookmarking your blogs, as doc for Angular.
angular is a powerful framework but it's a shame the documentation is so poor

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