Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Erik Meier and Max Pappas and Reem Jaghlit
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Erik Meier Max Pappas Reem Jaghlit

Prevent Routing To Secondary View If Page Refresh In Angular 5.0.0

By
Published in Comments (5)

As I've been digging into the Angular Router, I've made a decision to try and push more state information into the Route. For example, I've decided to let the Route drive my modal windows using a secondary, named-outlet. This has a lot of benefits like URL-passing and easy parameterization. But, it also means that modal windows will be present upon page-refresh. In many cases, this is totally fine. But, some modal windows - like error alerts - should not be re-rendered if the user refreshes the page. To deal with this, I've created a CanActivate RouteGuard that will navigate away from a particular secondary view upon refresh, leaving the rest of the URL in tact.

Run this demo in my JavaScript Demos project on GitHub.

The first challenge in this scenario is trying to differentiate an explicit navigation from a page refresh. Meaning, is this view being rendered as part of the application bootstrap? Or, is this view being rendered because the user explicitly navigated to it? Luckily, the router contains the boolean flag, "navigated", to tell us if at least one successful navigation has taken place. From this, we can deduce that if "router.navigated" is false, it's a page refresh; and, if "router.navigated" is true, it's an explicit navigation.

Once we can differentiate "navigation" from "refresh," the second challenge is figuring out how to navigate away from the requested URL without completely overriding the request. Meaning, if the URL contains both primary and secondary outlet segments, how can we redirect to the same URL, minus a specific secondary outlet?

To do this, I had to get a little hacky - hopefully someone will have a better suggestion. In my CanActivate RouteGuard, I parse the requested URL into a UrlTree. Then, I walk down the UrlTree's "primary" component hierarchy, delete any children associated with the current named-outlet. Once this is done, I use the router to navigate to the resultant UrlTree.

Here's the RouteGuard that I came up with:

// Import the core angular services.
import { ActivatedRouteSnapshot } from "@angular/router";
import { CanActivate } from "@angular/router";
import { Injectable } from "@angular/core";
import { PRIMARY_OUTLET } from "@angular/router";
import { Router } from "@angular/router";
import { RouterStateSnapshot } from "@angular/router";
import { UrlTree } from "@angular/router";

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

@Injectable()
export class DoNotShowSecondaryOnRefreshGuard implements CanActivate {

	private router: Router;

	// I initialize the secondary-view route guard.
	constructor( router: Router ) {

		this.router = router;

	}

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

	// I determine if the requested route can be activated (ie, navigated to).
	public canActivate(
		activatedRouteSnapshot: ActivatedRouteSnapshot,
		routerStateSnapshot: RouterStateSnapshot
		) : boolean {

		// We don't want to render this secondary view on page-refresh. As such, if this
		// is a page-refresh, we'll navigate to the same URL less the secondary outlet.
		if ( this.isPageRefresh() ) {

			console.warn( "Secondary view not allowed on refresh." );
			this.router.navigateByUrl( this.getUrlWithoutSecondary( routerStateSnapshot ) );
			return( false );

		}

		return( true );

	}

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

	// I return the requested URL (as defined in the snapshot), less any the "secondary"
	// named-outlet segments.
	private getUrlWithoutSecondary( routerStateSnapshot: RouterStateSnapshot ) : UrlTree {

		var urlTree = this.router.parseUrl( routerStateSnapshot.url );
		var segment = urlTree.root;

		// Since the "secondary" outlet is known to be directly off the primary view
		// (ie, not nested within another named-outlet), we're going to walk down the
		// tree of primary outlets and delete any "secondary" children. This should
		// leave us with a UrlTree that contains everything that the original URL had,
		// less the "secondary" named-outlet.
		while ( segment && segment.children ) {

			delete( segment.children.secondary );

			segment = segment.children[ PRIMARY_OUTLET ];

		}

		return( urlTree );

	}


	// I determine if the current route-request is part of a page refresh.
	private isPageRefresh() : boolean {

		// If the router has yet to establish a single navigation, it means that this
		// navigation is the first attempt to reconcile the application state with the
		// URL state. Meaning, this is a page refresh.
		return( ! this.router.navigated );

	}

}

As you can see, if this is not a page refresh, the view is allowed to resolve and render. However, if the page is a refresh (as defined by router.navigated), I strip the "secondary" named-outlet from the UrlTree and redirect the user to the new URL.

In my app module's route configuration, I then associate this Route Guard with the "secondary" named-outlet:

// 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 { DoNotShowSecondaryOnRefreshGuard } from "./do-not-show-secondary-on-refresh.guard.ts";
import { MainViewComponent } from "./main-view.component";
import { SecondaryViewComponent } from "./secondary-view.component";

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

var routes: Routes = [
	{
		// NOTE: I am prefixing the entire app with "/app" because that makes routing
		// much easier to deal with (especially with secondary routes).
		// --
		// Read More: https://www.bennadel.com/blog/3346-named-outlets-require-non-empty-parent-route-segment-paths-in-angular-4-4-4.htm
		path: "app",
		children: [
			{
				path: "main",
				component: MainViewComponent
			},
			{
				path: "secondary",
				outlet: "secondary",
				component: SecondaryViewComponent,
				canActivate: [ DoNotShowSecondaryOnRefreshGuard ]
			}
		]
	}
];

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

@NgModule({
	bootstrap: [
		AppComponent
	],
	imports: [
		BrowserModule,
		RouterModule.forRoot(
			routes,
			{
				// Tell the router to use the HashLocationStrategy.
				useHash: true
			}
		)
	],
	declarations: [
		AppComponent,
		MainViewComponent,
		SecondaryViewComponent
	],
	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
		// }
		DoNotShowSecondaryOnRefreshGuard
	]
})
export class AppModule {
	// ...
}

My app component then contains both the primary outlet and the named outlet:

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

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

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			<a [routerLink]="[ '/app', { outlets: { primary: 'main' } } ]">
				Open Main View
			</a>
			&mdash;
			<a [routerLink]="[ '/app', { outlets: { primary: null } } ]">
				Close Main View
			</a>
		</p>

		<p>
			<a [routerLink]="[ '/app', { outlets: { secondary: 'secondary' } } ]">
				Open Secondary View
			</a>
			&mdash;
			<a [routerLink]="[ '/app', { outlets: { secondary: null } } ]">
				Close Secondary View
			</a>
		</p>

		<router-outlet></router-outlet>
		<router-outlet name="secondary"></router-outlet>
	`
})
export class AppComponent {
	// ...
}

The routable views don't really have any meaningful logic; so, I won't bother showing them. But, with our Route Guard in place, we can open the application and toggle both the primary and secondary outlets:

Preventing secondary view rendering on page refresh in Angular 5.0.0.

As you can see, during the course of normal operation, the secondary view is allowed to render. However, if we refresh the page while the secondary view is rendered, we can see that the Route Guard intercepts the request, denies it, and then navigates to a new URL:

Preventing secondary view rendering on page refresh in Angular 5.0.0.

Notice that while the "secondary" outlet has been removed, the Route Guard kept the rest of the URL pertaining to the primary outlet in tact.

Normally, if I want to navigate away from a view in Angular 5.0.0, I just use the "relativeTo" property of the .navigate() method and pass-in the ActivatedRoute instance. Unfortunately, I cannot substitute the Route Guard's ActivatedRouteSnapshot in the .navigate() method. As such, I have to fall-back to parsing the URL into a UrlTree. I have to assume there is an easier approach; I'm just not seeing it. In the meantime, this Route Guard seems to be working well for when I want to prevent a secondary view from rendering on page refresh.

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

Reader Comments

1 Comments

The problem with z-index is that very few people understand how it really works. It's not complicated, but it if you've never taken the time to read its specification, there are almost certainly crucial aspects that you're completely unaware of.

In the following HTML you have three <div> elements, and each <div> contains a single <span> element. Each <span> is given a background color - red, green, and blue respectively. Each <span> is also positioned absolutely near the top left of the document, slightly overlapping the other <span> elements so you can see which ones are stacked in front of which. The first <span> has a z-index value of 1, while the other two do not have any z-index set.

:)

15,848 Comments

@Willard M.,

Completely agree. And this was me for like the last 10-years. I had a vague understanding of how z-index worked; but mostly that higher z-index was over a lower z-index. I think part of my confusion does - in all fairness - stem from the fact that one of the browsers had some wonky rules for stacking waaaay back in the day. I vaguely remember having to mess with parent elements because children wouldn't stack unless their parents were also stacked. Of course, I could just be remembering incorrectly.

Regardless, you are right -- it's not really that complicated once you just think through the rules. Though, things like "opacity" creating a stacking context is a little surprising. Just something to be aware of.

As far as your thought-experiment, I assume the first Span with the z-index would be on top. Then, the latter two would stack under that, but in the document content order (since they do not have a z-index applied).

1 Comments

Will it work on angular 7? its keep on looping to same url (means this.router.navigated is false after this.router.navigateByUrl( this.getUrlWithoutSecondary( routerStateSnapshot ) );

15,848 Comments

@Thiru,

I just upgraded my demo from Angular 5 to Angular 7.2.15:

https://bennadel.github.io/JavaScript-Demos/demos/prevent-secondary-view-on-refresh-angular7/

... and it works perfectly well on my end -- no looping. Even in Angular 7, the .router.navigated Boolean comes back as false on the page refresh. As such, I am not sure why you are seeing a different behavior.

Perhaps it's a browser issue - what browser do you use? I have tested it on latest Chrome and Firefox and found no issues.

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