Configurable Guards & Resolvers
See original GitHub issueFeature 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!)
Issue Analytics
- State:
- Created 2 years ago
- Reactions:35
- Comments:5 (3 by maintainers)
Top GitHub Comments
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
InjectionToken
with thefactory
option. See this StackBlitz for an example: https://stackblitz.com/edit/configurable-resolver?file=src%2Fapp%2Fapp.module.tsLast I checked the
factory
option isn’t well-documented, but it’s very useful for cases like this.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.