Reputation: 185
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
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))
));
}
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
Reputation: 1503
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