Repository navigation
Configurable Guards & Resolvers #42171
Description
Feature Request
Relevant Package
This feature request is for @angular/router
Description
I want to use a guard or a resolver across the app in many places, but always with a slightly different configuration. E.g. a guard should redirect to different pages when some request fails or a resolver should load data from different endpoints.
The current solution is to either pass data in the route and let the guard/resolver access this data, or create a subclass of a guard / resolver for each specific configuration. However, there is no type-safety for route data and the data may be overwritten from a parent route when paramsInheritanceStrategy is active. Additionally, this can get really bloated when using several configured guards or resolvers:
// routes.ts
{
path: 'page-a',
component: PageAComponent,
data: {
authGuard: {
failureRedirect: '/login'
}
},
canActivate: [ AuthGuard ],
resolve: {
foo: FooContentResolver,
bar: BarContentResolver
}
},
{
path: 'page-b',
component: PageBComponent,
data: {
authGuard: {
failureRedirect: '/signup'
}
},
canActivate: [ AuthGuard ]
}// resolvers
@Injectable()
abstract class ContentResolver implements Resolve<any> {
// ...
}
@Injectable()
class FooContentResolver extends ContentResolver {
readonly options: IContentResolverOptions = {
url: '/api/foo',
method: 'get'
};
// ...
}
@Injectable()
class BarContentResolver extends ContentResolver {
readonly options: IContentResolverOptions = {
url: '/api/bar',
method: 'get'
};
// ...
}Describe the solution you'd like
To solve this issue, I'm imagining something similar to the forRoot/ ModuleWithProviders approach, that would let you pass options to some injectable (like here, a guard/resolver). So the canActivate, canActivateChild and canLoad attributes of a Route could also accept some kind of InjectableWithProviders object next to the InjectionToken / string.
// routes.ts
{
path: 'page-a',
component: PageAComponent,
canActivate: [ AuthGuard.withOptions({ failureRedirect: '/login' }) ],
resolve: {
foo: ContentResolver.load({
url: '/api/foo',
method: 'get'
}),
bar: ContentResolver.load({
url: '/api/bar',
method: 'get'
})
}
},
{
path: 'page-b',
component: PageBComponent,
canActivate: [ AuthGuard.withOptions({ failureRedirect: '/signup' }) ]
}const AUTH_GUARD_OPTIONS = new InjectionToken<IAuthGuardOptions>('auth-guard-options');
@Injectable()
class AuthGuard implements CanActivate {
static withOptions(options: IAuthGuardOptions): InjectableWithProviders<AuthGuard> {
return {
injectable: AuthGuard,
providers: [
{ provide: AUTH_GUARD_OPTIONS, useValue: options }
]
};
}
// ...
}const CONTENT_RESOLVER_OPTIONS = new InjectionToken<IContentResolverOptions>('content-resolver-options');
@Injectable()
class ContentResolver implements Resolve<any> {
static load(options: IContentResolverOptions): InjectableWithProviders<ContentResolver> {
return {
injectable: ContentResolver,
providers: [
{ provide: CONTENT_RESOLVER_OPTIONS, useValue: options }
]
};
}
// ...
}Describe alternatives you've considered
An alternative would be to use something similar to MAT_DIALOG_DATA used in @angular/material. We wouldn't need a fully flexible InjectableWithProviders, but every guard / resolver could inject a common token, e.g. GUARD_OPTIONS / RESOVLER_OPTIONS that should be configurable in the route, e.g.:
// routes.ts
{
path: 'page-a',
component: PageAComponent,
canActivate: [
{ guard: AuthGuard, options: { failureRedirect: '/login' } }
],
resolve: {
foo: {
resolver: ContentResolver,
options: {
url: '/api/foo',
method: 'get'
}
},
bar: {
resolver: ContentResolver,
options: {
url: '/api/bar',
method: 'get'
}
}
}
},
{
path: 'page-b',
component: PageBComponent,
canActivate: [
{ guard: AuthGuard, options: { failureRedirect: '/signup' } }
]
}I also tried to dynamically create a subclass in the static method and return it, but I assume this is not optimal for dependency discovery (and is currently failing with an error when using dependencies in the constructor of the guard: Error: This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.):
@Injectable()
class AuthGuard implements CanActivate {
static withOptions(options: IAuthGuardOptions) {
@Injectable({ providedIn: 'root' })
class ConfiguredAuthGuard extends AuthGuard {
_options = options;
}
return ConfiguredAuthGuard
}
// ...
}Related Issues
#18247 seems related to this, but suggests some more substantial changes to routes & guards in general (which I would like to see as well!)
Activity
angular-robot commentedon Jun 5, 2021
This feature request is now candidate for our backlog! In the next phase, the community has 60 days to upvote. If the request receives more than 20 upvotes, we'll move it to our consideration list.
You can find more details about the feature request process in our documentation.
sg-gdarnell commentedon Jun 16, 2021
I don't think the object-oriented solution that you mentioned is really that bad, but I suppose I could see it being a pain if you had like 50 of them.
That said, you can already accomplish this today using an
InjectionTokenwith thefactoryoption. See this StackBlitz for an example: https://stackblitz.com/edit/configurable-resolver?file=src%2Fapp%2Fapp.module.tsLast I checked the
factoryoption isn't well-documented, but it's very useful for cases like this.SchnWalter commentedon Jul 2, 2021
You can use the
Route.datato pass configuration to resolvers and guards, it's always available in the snapshot. But there is one problem: you can't use more than one resolver instance, because there's no way to know under which key the InjectionToken is located.You can blame
resolveNode()for that:angular/packages/router/src/operators/resolve_data.ts
Lines 66 to 69 in e99af18
This can have a backwards compatible fix in 2 different ways:
By introducing an optional key argument to the resolve method:
The change required to implement this is minimal:
getResolver()needs to receive the key and pass it along to the resolver.By changing the
Route.resolveto accept the currentResolveData(a map of injection tokens) or a single injection token, for which the service will have to return multiple keys to add to the data object (bothfooandbar`).Here, we just need to figure out a way to distinguish between a plain object for
ResolveDataand the full spectrum of values allowed the injection token (which isany, seeResolveData). -- and we probably need to overload theResolve.resolve()method for this "data map" scenario.atscott commentedon Sep 20, 2022
Closing as resolved by the feature which allows guards as plain functions (0abb67a). You can now accomplish this configurability by having a factory function that accepts configuration and returns a guard/resolver function.
angular-automatic-lock-bot commentedon Oct 21, 2022
This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.