63

I'm new to angular, I want stop the routing when user clicks on refresh button or back button based on some condition. I don't know whether this is possible, if anybody knows help me

constructor(private route: Router) {
    this.route.events
        .filter(event => event instanceof NavigationEnd)
        .pairwise().subscribe((event) => {
            if (expression === true){
                // stop routing 
            } else {
                // continue routing
            }      
        });
}

Can it be possible? If yes, how can I do this?

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Gavishiddappa Gadagi
  • 1,120
  • 1
  • 16
  • 34

5 Answers5

59

I stumbled upon this question quite a bit after the fact, but I hope to help someone else coming here.

The principal candidate for a solution is a route guard.

See here for an explanation: https://angular.io/guide/router#candeactivate-handling-unsaved-changes

The relevant part (copied almost verbatim) is this implementation:

import { Injectable }           from '@angular/core';
import { Observable }           from 'rxjs';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot }  from '@angular/router';

import { MyComponent} from './my-component/my-component.component';

@Injectable({ providedIn: 'root' })
export class CanDeactivateGuard implements CanDeactivate<MyComponent> {

  canDeactivate(
    component: MyComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    // you can just return true or false synchronously
    if (expression === true) {
      return true;
    }
    // or, you can also handle the guard asynchronously, e.g.
    // asking the user for confirmation.
    return component.dialogService.confirm('Discard changes?');
  }
}

Where MyComponent is your custom component and CanDeactivateGuard is going to be registered in your AppModule in the providers section and, more importantly, in your routing config in the canDeactivate array property:

{
  path: 'somePath',
  component: MyComponent,
  canDeactivate: [CanDeactivateGuard]
},

Edit: Given that this question has quite a bit of traction and that many people seems to be requesting a solution to a different but related problem (how to add guards to a set of routes), I'm adding here a small helper function to add a set of guards to all of them:

// the Pick<...> syntax could be extended or replaced with a Partial<Route>
function withGuards<T extends Route | Routes>(input: T,
    guards: Pick<Route, 'canActivate' | 'canDeactivate'>): T {
  const routes: Route[] = Array.isArray(input) ? input : [input];
  for (let route of routes) {
    // if an object already exists, we do not replace the configured guards. meant to allow overrides for specific entries.
    Object.entries(guards).forEach(([prop, guard]) => {
      if (route && !route[prop]) route[prop] = guard;
    });
    if (route?.children) {
      withGuards(route.children, guards);
    }
  }

  return input;
}

// usage:

const route = withGuards({
    path: 'somePath',
    component: MyComponent
  }, {
    canDeactivate: [CanDeactivateGuard]
  });

This method will inject the guard in all the provided route(s) and in all their nested children, recursively.

Alberto Chiesa
  • 7,022
  • 2
  • 26
  • 53
  • 1
    The downside is that we cannot apply guards globally to all routes without adding the guard to each and every route individually. – yankee May 31 '21 at 05:03
  • 3
    @yankee well, if you _really_ need to have your guard applied to every route, you can just preprocess the `ROUTES` constant before passing it into `RoutingModule.forRoot(ROUTES)`. A simple `ROUTES.foreach(r => r.canDeactivate = [ CanDeactivateGuard ])` or something similar would do. You could handle every other case (nested children routes, other guard types) with similar variations on this. – Alberto Chiesa May 31 '21 at 09:15
  • This does not work anymore. I am getting `NullInjectorError` did I did anything wrong? The only change I did, is that I registered my guard in `RouterModule.forChild` within lazy loaded module, where the component is used. – Akxe Nov 11 '21 at 23:18
  • @Akxe, this still works. I strongly suspect there is something wrong in your setup. The injector has everything to do with lazy loading, and probably nothing to do with guards. If you can't make it work, open another question! – Alberto Chiesa Nov 12 '21 at 09:06
  • In your answer, you never informed, that `CanDeactivateGuard` must be in `providers` array of a NgModule – Akxe Nov 12 '21 at 14:44
  • That doesn't seem to be very constructive. Every service in Angular (Guards are just a specialized service) need to be included in the Providers or being `providedIn: "root"`. I'm going to update the answer anyway, just in case anyone else had the same problem. – Alberto Chiesa Nov 12 '21 at 15:31
  • Is it possible to share code of confirm function of dialogService. As for me async not working. Thanks! – Rajan Kashiyani Jun 09 '22 at 14:00
  • @AlbertoChiesa This is the best approach if you really need all routes. yet still kind of cluttered in a nested lazy loaded routes scenario. I'd wish for something like `this.Router.events.forEach((event) => { if (event instanceof NavigationStart) { event.cancel(); } });` – Martin Eckleben Dec 08 '22 at 07:19
  • @MartinEckleben I don't agree. The "code driven" approach you suggest is invisible to other developers looking at the application routes. I have updated the answer with an helper that would be more explicit. – Alberto Chiesa Dec 09 '22 at 16:55
5

There is another solution which I invented and it works:

(For lazy people like me who do not want to create guard for handling this)

import { NavigationStart, Router } from '@angular/router';

In constructor:

constructor(private router: Router) {
    router.events.forEach((event) => {
      if (event instanceof NavigationStart) {
        /* write your own condition here */
        if(condition){
          this.router.navigate(['/my-current-route']); 
        }
      }
    });
  }

Hopefully you won't be lazy enough to change '/my-current-route' to your route url.

Hari Das
  • 10,145
  • 7
  • 62
  • 59
  • This is searching for unexpected problems, in my own experience. When a navigation is running, the router can get finicky in handling new navigation requests. IMHO this is the wrong kind of lazy. – Alberto Chiesa Oct 28 '21 at 06:17
  • 1
    I won't give a +1 on this one, but I liked the last sentence though. :D – Guillaume PETIT May 13 '22 at 06:42
  • My colleague left this gem. He's on a different project now. It didn't smell good, so I searched for another solution. I can bet he got it from here, as he is exactly the type that says "it's good because it works". Love the humor in the last sentence :D – bokkie May 20 '22 at 20:59
  • This can also lead to a cancellation of the whole navigation, see [Angular 2: NavigationCancel - Navigation ID 2 is not equal to the current navigation id 3](https://stackoverflow.com/q/41937115/1456376). – insertusernamehere Oct 06 '22 at 07:38
4

The easy way for me is with skiplocationchange in a new route navigate like this:

if(condition === true){

  const currentRoute = this.router.routerState;

  this.router.navigateByUrl(currentRoute.snapshot.url, { skipLocationChange: true });
  // this try to go to currentRoute.url but don't change the location.

}else{
  // do nothing;
}

is a no beautiful method but works

Javier López
  • 99
  • 1
  • 5
2

I had a similar challenge trying to include a checkbox that would programmatically enable and disable routing.

I tried the solutions of to navigateByUrl to the same location but it seemed a bit inconsistent when trying to navigate to any other routes afterwards

So instead I created two route guards, and programmatically reset the route config when the checkbox was toggled

Guards

@Injectable({ providedIn: "root" })
export class RouterDisableGuard implements CanActivate {
  canActivate() {
    return false;
  }
}

@Injectable({ providedIn: "root" })
export class RouterEnableGuard implements CanActivate {
  canActivate() {
    return true;
  }
}

Service

this.router.resetConfig(
      this.router.config.map((r) => {
        if (!r.redirectTo) {
          r.canActivate = enabled ? [RouterEnableGuard] : [RouterDisableGuard];
        }
        return r;
      })
    );
chrismclarke
  • 1,995
  • 10
  • 16
-9

You can use NavigationStart event like the following code snippet.

this.router.events.subscribe( (event: any) => {
   if (event instanceOf NavigationStart) {
      /// Your Logic
   }
})
KAUSHIK PARMAR
  • 535
  • 6
  • 11
  • 5
    This is an event handler for when the navigation already started. Would be better to not fire this if you want to prevent the NavigationStart. – Edgar Quintero Jul 15 '19 at 17:42