Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at FirstMark Tech Summit (New York, NY) with: Atticus White
Ben Nadel at FirstMark Tech Summit (New York, NY) with: Atticus White@atticoos )

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

By Ben Nadel 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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader 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!

Reply to this Comment

@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

Reply to this Comment

Nice tutorial. Is it possible refresh the page after routing back to parent component?

Reply to this Comment

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

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
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.