Hristo
Hristo

Reputation: 185

Set URL to the cancelled path when redirecting to the 404 page from a resolver

Let's say we have the following route configuration:

[{
    path: "people",
    children: [
        {
            path: ":id",
            component: PersonProfileComponent,
            resolve: { person: PersonResolver },
            children: [
                { path: "", component: PersonOverviewComponent },
                { path: "photos", component: PersonPhotoListComponent }
            ]
        }
    ]
},
{ path: "404", component: PageNotFoundComponent },
{ path: "**",  component: PageNotFoundComponent }]

I want to redirect to the 404 page when user try to navigate to any of these:

/people/{non-existing-id}
/people/{non-existing-id}/photos
/people/{existing-id}/{non-existing-subpage}

The third case is handled by the ** route since it's not matched by any of the previous routes. So the PersonResolver has to handle the first two cases.

export class PersonResolver implements Resolve<Person> {
    constructor(private peopleService: PeopleService, private router: Router) { }

    resolve(route: ActivatedRouteSnapshot): Observable<Person> {
        return this.peopleService.find(id).catch(error => {
            if (error instanceof Response) {
                if (error.status === 404) {
                    this.router.navigate(["/404"]);
                }
                else {
                    //todo
                }
            }
            else {
                //todo
            }
            return Observable.of(null);
        });
    }
}

Everything works but the problem is that when the 404 page is shown the browser URL is updated to <hostname>/404 (as expected). My question is how to set the URL to the path we've just cancelled (eg. <hostname>/people/fake-id/photos) as this is how 404 is handled all over the web. (I also tried passing {skipLocationChange: true} to router.navigate() but this keeps the location before that we are trying to navigate to)

Upvotes: 7

Views: 1961

Answers (2)

zurfyx
zurfyx

Reputation: 32807

@Víťa Plšek was really good for inspiration, but it did not work for me.

Here's how I did:

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): Observable<any> | Promise<any> | any {
    // Do some stuff that fails...
    // Redirect to 404.
    return this.router.events
      .filter(event => event instanceof NavigationEnd)
      .first()
      .subscribe((event: NavigationError) => (
        this.router.navigate(['/404'], {skipLocationChange: true})
        .then(() =>  this.location.go(event.url))
      ));
  }

Cleaner version:

my-component-resolver.ts

@Injectable()
export class YoutubeVideoResolver {

  constructor(
    private resolverNavigation: ResolverNavigation,
    private router: Router,
    private http: Http,
  ) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): Observable<any> | Promise<any> | any {
    this.http.get('/foo')
      .map((response) => {
        // Found. Do something with the data.
      })
      .catch((error) => {
        // Not found. Redirect to 404.
        this.resolverNavigation.to404();
        return Observable.of(null);
      });
  }
}

resolver-navigation.ts

import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import {
  Router,
  NavigationEnd,
} from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class ResolverNavigation {
  constructor(
    private router: Router,
    private location: Location,
  ) {}

  to404(): void {
    this.router.events
      .filter(event => event instanceof NavigationEnd)
      .first()
      .subscribe((event: NavigationEnd) => (
        this.router.navigate(['/404'], {skipLocationChange: true})
          .then(() =>  this.location.go(event.url))
      ));
  }
}

Upvotes: 2

Its possible to change url without navigating using Location - docs

You can react on error with subscribing to Router.events - NavigationError and then show 404 with skipLocationChange and then change location with location.go

You have to change url after navigation to 404 is complete.

  constructor(router: Router, location: Location) {
    router.events
      .filter(event => event instanceof NavigationError)
      .subscribe((event: NavigationError) => {
        router.navigate(["/404"], {skipLocationChange: true})
          .then(() =>  location.go(event.url));
      })
  }

I would prefer doing this in my routing module, but it could be done also in resolver.

Router.events are not very well documented yet, you can check at least list of them here and do some logging.

Upvotes: 8

Related Questions