angular: NavigationExtras when returning URLTree from guard
đ feature request
Relevant Package
This feature request is for @angular/router
Description
In #26521 (Angular 7.1.0), a feature was introduced that allows returning a URLTree
from guards in order to initiate a redirect. However, this doesnât seem to allow using NavigationExtras
such as skipLocationChange
.
Since the change in #26521 wasnât just syntax sugar, but is relevant to have predictable behavior when dealing with multiple guards, it should be considered to somehow allow using these options when returning a URLTree
as well.
Describe the solution youâd like
I donât have a really good proposal at this moment apart from next to returning URLTree
also returning some kind of
// TODO: Find a better name?
interface NavigationOptions {
urlTree: URLTree;
navigationExtras: NavigationExtras;
}
such that I could use
class AwesomeGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): NavigationOptions | Observable<NavigationOptions> {
return {
urlTree: this.router.parseUrl("âŚ"),
navigationExtras: {
skipLocationChange: true,
},
};
}
}
I guess the exact typing of the guard would have to be
// Just a placeholder name
type GuardReturnType = boolean | URLTree | NavigationOptions;
interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
GuardReturnType
| Promise<GuardReturnType>
| Observable<GuardReturnType>
}
Admittedly, the return type of this function is growing increasingly complex with this.
Describe alternatives youâve considered
Possible options that I donât think would make good solutions would be
- Returning an array
[URLTree, NavigationExtras]
(this just feels unclean) - Extending URLTree with the navigationExtras and extend parseUrl to take them as well (this muddies the entire API for a change in a specific location)
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 97
- Comments: 45 (18 by maintainers)
Commits related to this issue
- fix(router): adjust UrlTree redirect to replace URL if in eager update Without this change when using UrlTree redirects in `urlUpdateStrategy="eager"`, the URL would get updated to the target locatio... — committed to jasonaden/angular by jasonaden 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update Without this change when using UrlTree redirects in `urlUpdateStrategy="eager"`, the URL would get updated to the target locatio... — committed to jasonaden/angular by jasonaden 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update Without this change when using UrlTree redirects in `urlUpdateStrategy="eager"`, the URL would get updated to the target locatio... — committed to jasonaden/angular by jasonaden 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update (#31168) Without this change when using UrlTree redirects in `urlUpdateStrategy="eager"`, the URL would get updated to the targe... — committed to angular/angular by jasonaden 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update (#31168) Without this change when using UrlTree redirects in `urlUpdateStrategy="eager"`, the URL would get updated to the targe... — committed to angular/angular by jasonaden 5 years ago
- docs(router): clarify that createUrlTree only uses some NavigationExtras (#33029) There is some confusion around which `NavigationExtras` values are used by createUrlTree. This specifies that only va... — committed to angular/angular by deleted user 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update (#32988) Resubmit #31168 now that google3 tests can pass. This requires http://cl/272696717 to be patched. Original description ... — committed to angular/angular by deleted user 5 years ago
- docs(router): clarify that createUrlTree only uses some NavigationExtras (#33029) There is some confusion around which `NavigationExtras` values are used by createUrlTree. This specifies that only va... — committed to ODAVING/angular by deleted user 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update (#32988) Resubmit #31168 now that google3 tests can pass. This requires http://cl/272696717 to be patched. Original description ... — committed to ODAVING/angular by deleted user 5 years ago
- fix(router): adjust UrlTree redirect to replace URL if in eager update (#32988) Resubmit #31168 now that google3 tests can pass. This requires http://cl/272696717 to be patched. Original description ... — committed to AndrusGerman/angular by deleted user 5 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
- feat(router): Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards Returning `UrlTree` from a guard was a convenient new feature added to the `Router`. However, it does not ha... — committed to atscott/angular by atscott 3 years ago
+1 to this issue as having navigationExtras when creating a UrlTree allows canActivate to preserve extras like state if the url is changed.
In addition, it would be nice if thereâs a way to preserve the navigation extras when returning a url tree from CanActivate.
@Airblader @jayoungers @scott-ho @axelboc
Thanks for all the comments on this issue. I want to get this resolved somehow in the very near future. Ideally in the
RC
period of version 8 (there might be a bit of an issue doing so because it would be adding a feature).Iâm happy to see the
404
and301
examples above from @axelboc.We were looking at an issue internally and found that, with the redirects being done the way they currently are, the
back
button is essentially broken. I was looking at making a fix such that returning aUrlTree
will cause the same redirect, but will always be done withreplaceUrl
. This would be turned on by default, because without it hittingback
will end up redirecting again, and the user will not be able to goback
past the page they redirected to.I would love some feedback from anyone using this feature or wanting to use this feature. We can get this implemented quickly and pushed out as part of
RC
as afix
rather than the full feature described above.That being said, the original design did contain an option to provide
NavigationExtras
but we didnât have concrete use cases where it would be needed. Now, with those provided, we should be able to implement this API as well.Two concrete use cases I have throughout my project in
canActivate
guards:1) Redirecting to a 404 page when trying to access an unknown resource
skipLocationChange
: the user should see the URL of the unknown resource, so they can fix a typo in the URL and try again, for instance.UrlTree
technique, at the end of the redirect, the URL shows/404
and the user can navigate back.2) Performing a 301 redirect to a resourceâs canonical URL
replaceUrl
: the user should see the canonical URL without being able to navigate back to the obsolete/aliased URL.UrlTree
technique, the user can navigate back to the resourceâs obsolete/aliased URL.@jasonaden Thanks for the reply! Our concrete usecase is e.g. a 403 redirect where we want to show an error page but retain the URL.
So no, the forwarding of the navigation extras wouldnât help us here, unfortunately.
I was confused that this wasnât implemented because of the function createUrlTree on the router, which has the NavigationExtras as a parameter. I misunderstood what that function did.
Just to add my own case, I had a CanActivate guard verifying that the user is part of a certain group before accessing a page. If he isnât, then redirect to an AccessDenied page that can display the group which the user lacks. The group name wouldâve been passed to the AccessDenied page via NavigationExtrasâ state within the guard, since the guard is better positioned to know the group name, and the AccessDenied component may potentially be used in different contexts, by different guards.
Hope that makes sense.
Hi, no progress or ETA on this, but here to provide some alternative workaround hackery for the
skipLocationChange
redirect:TL;DR - for the
skipLocationChange
case, you can add this one-liner before thereturn <urlTree>
in your guard:this.router.getCurrentNavigation().extras.skipLocationChange = true;
Please add a test for this in withRouterTestingModule
Two pre-understanding points:
Router
uses theUrlTree
for redirecting, it inherits theskipLocationChange
value from the last request (code reference.currentNavigation
property in theRouter
gets the extras from the navigation request without doing a deep copy (code reference)Because of the above two points, you can actually simply do
this.router.getCurrentNavigation().extras.skipLocationChange = true;
before thereturn <urlTree>
in your guard. Doing this certainly depends on theRouter
implementation details so please add a test for your guard usingRouterTestingModule
and understand that it is possible that this could break with changes to theRouter
code (and those changes would not be considered breaking changes since the internal implementation isnât public API).Please donât forget about some sort of feature parity with
Resolve<T>
when considering changes to URLTrees from guards.Being able to redirect elegantly from a Resolver is very important - especially in the case where an invalid URL is provided (eg. a mistyped ID).
IMO, the developer defined state that gets added to the NavigationExtras should also be passed through when returning a UrlTree. Otherwise this additional information gets lost during the redirect
+1 for this feature request, I was honestly surprised that you canât utilize anything like navigationExtras in a guard when you can do it everywhere else.
I was hoping to build some process validation into my navigation guards to prevent users from getting clever and trying to bypass the usual workflow by changing the URL. I can still do that the way thing are now, but I canât funnel any custom data back through the navigationExtras in a neat and readable way.
I was initially thinking a good solution would be to provide some sort of âalternateRouterCommandâ interface on the guard itself, but I think I might have a better solution that covers more scenarios. Would it be reasonable to provide an âonGuardedâ callback property on a Route that aggregates the values returned by negative guards into an array until all guards have been processed, then calls the function you provide in the âonGuardedâ property with the array of values returned from failed guards, target route, and current route? That would allow developers to build dynamic responses from rejected routes, predict the order in which guards will run, and put a lot of their error handling in a neat, centralized location.
Similar to other mentioned use cases, we would like to preserve the URL for error pages:
ForbiddenComponent
with error message (known by guard)/user/:id
), but a resource does not exist (or has been removed. A guard could be used to show aNotFound
componentWe also use a lot of redirects currently to populate query parameters. It would be great if it was possible to redirect and add missing parameters:
So basically, all options from NavigationExtras except
relativeTo
,queryParams
, andfragment
(since these are handled byrouter.createUrlTree
) would be great to have IMO. đThe use case I have is similar to https://github.com/angular/angular/issues/17004 - being able to render a 404 response for the current route. I donât want to redirect to a â404â / ânot-foundâ route.
Hey, wanted to know the status of this feature request. No update for few months now, was it missed again?
Just stumbled upon this issue, I guess the only workaround now to not break back button is to return false and schedule a navigation in set timeout in a guard.
Interesting to see that not many people are hitting this issue
EDIT: This seems to work as well in canActivate:
EDIT: added delay(1) operator, apparently it is needed to avoid duplicate redirect/events at least when HashLocationStrategy is used (#16710)
@jasonaden the v8 fix youâre suggesting sounds great. Youâre right, it would at least fix the back button for now.
@timminss not a good one, Iâm afraid. We did something similar to the below code snippet, but using a generic âerrorâ page for various errors instead of a specific route for each error. The errors generated are from the
HttpClient
when resolving data, so they haderr.status
that we could check for routing purposes.This was the best we could come up with 2-3 years ago when we wrote the code. We werenât able to find much documentation for more advanced routing use-cases at the time.
You may be better off with some sort of view cover indicator that exists at a root level to indicate the route is loading: I think youâll run into issues eventually for more complicated scenarios with redirecting from the intended route and back
My apologies on this. It should have been in 8, but got missed. See above for the fix. This should land shortly.
@Airblader good point.
the current code is using a serialized url to redirecting, see the source.
@jasonaden Concrete use cases are not the major concern for the feature request. There are so many possibilities.
would that cause problems in the application?
that would be an application problem but not a framework problem.Since for the first glance, you donât have valid point about what will be bad for the framework. I believe we could move on to make this happen.
But the benefit and entire point of allowing returning a tree is that it makes how multiple guards interact predictable and stable. This feature wasnât about specific usecases. Iâd like to start returning URL tree in as many cases as I can (whenever I donât return true).
I see. I think the answer comes down to what the intention of returning
UrlTree
is:Iâm looking at is as though the requested route is not valid under any circumstance. If weâre looking at it as a short hand for the navigate method, then what youâre saying makes perfect sense.
In my mind I would have had the functionality setup so that if
UrlTree
is returned, then that is the route the user intended to go to (the guard result wasnâttrue
, it wasnâtfalse
, it just lead to the correct path).In scenarios such as authentication for example, where the user isnât logged in or the session has expired, I donât think
UrlTree
is the correct response: it should befalse
(this route is valid, but you donât have access to it), and the guardrouter.navigate
s to the login page.The unexpected part is that the navigation works differently than any other navigation where I donât set any navigation extras. IMHO here Angular would suddenly dictate opinion over what navigation does or doesnât make sense, and I would like to avoid that. Everywhere else the user gets to choose the options, so it should be the same here. Failing that, it should at least behave the same way as everywhere else (which is the current behavior).
I had a whole comment ready to go to post on the primary issue (#24618) assuming this was a bug until I ran into this issue: in my opinion I would say the default use case should be for it to consider this a redirectTo and
skipLocationChange
- Iâm not sure under what scenarios youâd want to keep the original route.Here are the main scenarios Iâm returning
UrlTree
currently:The application is multi-tenant, where each user is a member of various tenants. If a user enters the site at the base
/
URL, my authorization guard will determine which tenants they are a member of and redirect the user to the last tenant they were viewing (e.g.tenantA
).Result: user has entries of
/
and/tenantA
in their browser history and can navigate back to/
Shortened links: Letâs say I have a guard that will transform the url
/tenantA/subentities/456
to the url/tenantA/entities/123/subentities/456
(via an API call that determined the parent id). The reason being other systems may not know the parententity
id.Result: user has entries of
/tenantA/subentities/456
and/tenantA/entities/123/subentities/456
, allowing them to navigate back to the invalid urlLink repair: using the same example from above, given the url
/tenantA/entities/123/subentities/456
, if theentities
id was really 222, the guard can return a repaired url of/tenantA/entities/222/subentities/456
(in these two use cases thesubentity
is truly the primary entity we care about, and theentities
level is more of a vanity hierarchical collection)A similar example could be around formatting (i.e. if your route has a date like
/1-1-2000/
and you redirect to standardize it to/01-01-2000/
)In all of these examples I wouldnât want the original url to exist as valid locations in the browser