docs: Deprecate class and InjectionToken and resolvers (#47924)

Class and `InjectionToken`-based guards and resolvers are not as configurable,
are less re-usable, require more boilerplate, cannot be defined inline with the route,
and require more in-depth knowledge of Angular features (`Injectable`/providers).
In short, they're less powerful and more cumbersome.

In addition, continued support increases the API surface which in turn increases
bundle size, code complexity, the learning curve and API surface to teach,
maintenance cost, and cognitive load (needing to grok several different types
of information in a single place).

Lastly, supporting only the `CanXFn` types for guards and `ResolveFn` type
for resolvers in the `Route` interface will enable better code
completion and integration with TypeScript. For example, when writing an
inline functional resolver today, the function is typed as `any` and
does not provide completions for the `ResolveFn` parameters. By
restricting the type to only `ResolveFn`, in the example below
TypeScript would be able to correctly identify the `route` parameter as
`ActivatedRouteSnapshot` and when authoring the inline route, the
language service would be able to autocomplete the function parameters.

```
const userRoute: Route = {
  path: 'user/:id',
  resolve: {
    "user": (route) => inject(UserService).getUser(route.params['id']);
  }
};
```

Importantly, this deprecation only affects the support for class and
`InjectionToken` guards at the `Route` definition. `Injectable` classes
and `InjectionToken` providers are _not_ being deprecated in the general
sense. Functional guards are robust enough to even support the existing
class-based guards through a transform:

```
function mapToCanMatch(providers: Array<Type<{canMatch: CanMatchFn}>>): CanMatchFn[] {
  return providers.map(provider => (...params) => inject(provider).canMatch(...params));
}
const route = {
  path: 'admin',
  canMatch: mapToCanMatch([AdminGuard]),
};
```

With regards to tests, because of the ability to map `Injectable`
classes to guard functions as outlined above, nothing _needs_ to change
if projects prefer testing guards the way they do today. Functional
guards can also be written in a way that's either testable with
`runInContext` or by passing mock implementations of dependencies.
For example:

```
export function myGuardWithMockableDeps(
  dep1 = inject(MyService),
  dep2 = inject(MyService2),
  dep3 = inject(MyService3),
) { }

const route = {
  path: 'admin',
  canActivate: [() => myGuardWithMockableDeps()]
}

// test file
const guardResultWithMockDeps = myGuardWithMockableDeps(mockService1, mockService2, mockService3);
const guardResultWithRealDeps = TestBed.inject(EnvironmentInjector).runInContext(myGuardWithMockableDeps);
```

DEPRECATED: Class and `InjectionToken` guards and resolvers are
deprecated. Instead, write guards as plain JavaScript functions and
inject dependencies with `inject` from `@angular/core`.

PR Close #47924
This commit is contained in:
Andrew Scott 2022-10-31 09:13:05 -07:00 committed by Andrew Kushnir
parent 7f68a709a7
commit 926c35f4ac
5 changed files with 61 additions and 26 deletions

View file

@ -117,6 +117,7 @@ v15 - v18
| `@angular/router` | [`RouterLinkWithHref` directive](#router) | v15 | v17 |
| `@angular/router` | [Router writeable properties](#router-writable-properties) | v15.1 | v17 |
| `@angular/router` | [Router CanLoad guards](#router-can-load) | v15.1 | v17 |
| `@angular/router` | [class and `InjectionToken` guards and resolvers](#router) | v15.2 | v17 |
### Deprecated features with no planned removal version
@ -203,6 +204,8 @@ In the [API reference section](api) of this site, deprecated APIs are indicated
| [`RouterLinkWithHref` directive](api/router/RouterLinkWithHref) | Use `RouterLink` instead. | v15 | The `RouterLinkWithHref` directive code was merged into `RouterLink`. Now the `RouterLink` directive can be used for all elements that have `routerLink` attribute. |
| [`provideRoutes` function](api/router/provideRoutes) | Use `ROUTES` `InjectionToken` instead. | v15 | The `provideRoutes` helper function is minimally useful and can be unintentionally used instead of `provideRouter` due to similar spelling. |
| [`setupTestingRouter` function](api/router/testing/setupTestingRouter) | Use `provideRouter` or `RouterTestingModule` instead. | v15.1 | The `setupTestingRouter` function is not necessary. The `Router` is initialized based on the DI configuration in tests as it would be in production. |
| [class and `InjectionToken` guards and resolvers](api/router/DeprecatedGuard) | Use plain JavaScript functions instead. | v15.2 | Functional guards are simpler and more powerful than class and token-based guards. |
<a id="platform-browser"></a>

View file

@ -24,6 +24,7 @@ import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
import { Provider } from '@angular/core';
import { ProviderToken } from '@angular/core';
import { QueryList } from '@angular/core';
import { Renderer2 } from '@angular/core';
import { SimpleChanges } from '@angular/core';
@ -111,13 +112,13 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void;
}
// @public
// @public @deprecated
export interface CanActivate {
// (undocumented)
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}
// @public
// @public @deprecated
export interface CanActivateChild {
// (undocumented)
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
@ -129,7 +130,7 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou
// @public
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
// @public
// @public @deprecated
export interface CanDeactivate<T> {
// (undocumented)
canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
@ -147,7 +148,7 @@ export interface CanLoad {
// @public @deprecated
export type CanLoadFn = (route: Route, segments: UrlSegment[]) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
// @public
// @public @deprecated
export interface CanMatch {
// (undocumented)
canMatch(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
@ -237,6 +238,9 @@ export class DefaultUrlSerializer implements UrlSerializer {
serialize(tree: UrlTree): string;
}
// @public @deprecated
export type DeprecatedGuard = ProviderToken<any> | any;
// @public
export type DetachedRouteHandle = {};
@ -561,7 +565,7 @@ export function provideRoutes(routes: Routes): Provider[];
// @public
export type QueryParamsHandling = 'merge' | 'preserve' | '';
// @public
// @public @deprecated
export interface Resolve<T> {
// (undocumented)
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T;
@ -569,7 +573,7 @@ export interface Resolve<T> {
// @public
export type ResolveData = {
[key: string | symbol]: any | ResolveFn<unknown>;
[key: string | symbol]: ResolveFn<unknown> | DeprecatedGuard;
};
// @public
@ -611,12 +615,12 @@ export class ResolveStart extends RouterEvent {
// @public
export interface Route {
canActivate?: Array<CanActivateFn | any>;
canActivateChild?: Array<CanActivateChildFn | any>;
canDeactivate?: Array<CanDeactivateFn<any> | any>;
canActivate?: Array<CanActivateFn | DeprecatedGuard>;
canActivateChild?: Array<CanActivateChildFn | DeprecatedGuard>;
canDeactivate?: Array<CanDeactivateFn<any> | DeprecatedGuard>;
// @deprecated
canLoad?: Array<CanLoadFn | any>;
canMatch?: Array<Type<CanMatch> | InjectionToken<CanMatchFn> | CanMatchFn>;
canLoad?: Array<CanLoadFn | DeprecatedGuard>;
canMatch?: Array<CanMatchFn | DeprecatedGuard>;
children?: Routes;
component?: Type<any>;
data?: Data;

View file

@ -12,7 +12,7 @@ export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, DefaultExport, DeprecatedGuard, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {DebugTracingFeature, DisabledInitialNavigationFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, NavigationErrorHandlerFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withNavigationErrorHandler, withPreloading, withRouterConfig} from './provide_router';

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {EnvironmentInjector, EnvironmentProviders, InjectionToken, NgModuleFactory, Provider, Type} from '@angular/core';
import {EnvironmentInjector, EnvironmentProviders, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core';
import {Observable} from 'rxjs';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
@ -38,6 +38,23 @@ import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
*/
export type OnSameUrlNavigation = 'reload'|'ignore';
/**
* The `InjectionToken` and `@Injectable` classes for guards and resolvers are deprecated in favor
* of plain JavaScript functions instead.. Dependency injection can still be achieved using the
* `inject` function from `@angular/core`.
*
* @deprecated
* @see CanMatchFn
* @see CanLoadFn
* @see CanActivateFn
* @see CanActivateChildFn
* @see CanDeactivateFn
* @see ResolveFn
* @see inject
* @publicApi
*/
export type DeprecatedGuard = ProviderToken<any>|any;
/**
* Represents a route configuration for the Router service.
* An array of `Route` objects, used in `Router.config` and for nested route configurations
@ -110,7 +127,7 @@ export type Data = {
* @publicApi
*/
export type ResolveData = {
[key: string|symbol]: any|ResolveFn<unknown>
[key: string|symbol]: ResolveFn<unknown>|DeprecatedGuard
};
/**
@ -517,7 +534,7 @@ export interface Route {
* When using a function rather than DI tokens, the function can call `inject` to get any required
* dependencies. This `inject` call must be done in a synchronous context.
*/
canActivate?: Array<CanActivateFn|any>;
canActivate?: Array<CanActivateFn|DeprecatedGuard>;
/**
* An array of `CanMatchFn` or DI tokens used to look up `CanMatch()`
* handlers, in order to determine if the current user is allowed to
@ -526,7 +543,7 @@ export interface Route {
* When using a function rather than DI tokens, the function can call `inject` to get any required
* dependencies. This `inject` call must be done in a synchronous context.
*/
canMatch?: Array<Type<CanMatch>|InjectionToken<CanMatchFn>|CanMatchFn>;
canMatch?: Array<CanMatchFn|DeprecatedGuard>;
/**
* An array of `CanActivateChildFn` or DI tokens used to look up `CanActivateChild()` handlers,
* in order to determine if the current user is allowed to activate
@ -535,7 +552,7 @@ export interface Route {
* When using a function rather than DI tokens, the function can call `inject` to get any required
* dependencies. This `inject` call must be done in a synchronous context.
*/
canActivateChild?: Array<CanActivateChildFn|any>;
canActivateChild?: Array<CanActivateChildFn|DeprecatedGuard>;
/**
* An array of `CanDeactivateFn` or DI tokens used to look up `CanDeactivate()`
* handlers, in order to determine if the current user is allowed to
@ -544,7 +561,7 @@ export interface Route {
* When using a function rather than DI tokens, the function can call `inject` to get any required
* dependencies. This `inject` call must be done in a synchronous context.
*/
canDeactivate?: Array<CanDeactivateFn<any>|any>;
canDeactivate?: Array<CanDeactivateFn<any>|DeprecatedGuard>;
/**
* An array of `CanLoadFn` or DI tokens used to look up `CanLoad()`
* handlers, in order to determine if the current user is allowed to
@ -554,7 +571,7 @@ export interface Route {
* dependencies. This `inject` call must be done in a synchronous context.
* @deprecated Use `canMatch` instead
*/
canLoad?: Array<CanLoadFn|any>;
canLoad?: Array<CanLoadFn|DeprecatedGuard>;
/**
* Additional developer-defined data provided to the component via
* `ActivatedRoute`. By default, no additional data is passed.
@ -696,6 +713,8 @@ export interface LoadedRouterConfig {
* ```
*
* @publicApi
* @deprecated Use plain JavaScript functions instead.
* @see CanActivateFn
*/
export interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
@ -791,6 +810,8 @@ export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSn
* ```
*
* @publicApi
* @deprecated Use plain JavaScript functions instead.
* @see CanActivateChildFn
*/
export interface CanActivateChild {
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot):
@ -880,6 +901,8 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou
* ```
*
* @publicApi
* @deprecated Use plain JavaScript functions instead.
* @see CanDeactivateFn
*/
export interface CanDeactivate<T> {
canDeactivate(
@ -982,6 +1005,8 @@ export type CanDeactivateFn<T> =
* ```
*
* @publicApi
* @deprecated Use plain JavaScript functions instead.
* @see CanMatchFn
*/
export interface CanMatch {
canMatch(route: Route, segments: UrlSegment[]):
@ -1112,6 +1137,8 @@ export type CanMatchFn = (route: Route, segments: UrlSegment[]) =>
* The order of execution is: BaseGuard, ChildGuard, BaseDataResolver, ChildDataResolver.
*
* @publicApi
* @deprecated Use plain JavaScript functions instead.
* @see ResolveFn
*/
export interface Resolve<T> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T>|Promise<T>|T;
@ -1196,7 +1223,7 @@ export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSna
* ```
*
* @publicApi
* @deprecated Use `CanMatch` instead
* @deprecated Use `CanMatchFn` instead
*/
export interface CanLoad {
canLoad(route: Route, segments: UrlSegment[]):

View file

@ -11,7 +11,7 @@ import {concat, defer, from, MonoTypeOperatorFunction, Observable, of, OperatorF
import {concatMap, first, map, mergeMap, tap} from 'rxjs/operators';
import {ActivationStart, ChildActivationStart, Event} from '../events';
import {CanActivateChild, CanActivateChildFn, CanActivateFn, Route} from '../models';
import {CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Route} from '../models';
import {redirectingNavigationError} from '../navigation_canceling_error';
import {NavigationTransition} from '../navigation_transition';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
@ -142,7 +142,8 @@ function runCanActivateChild(
getTokenOrFunctionIdentity<CanActivateChild>(canActivateChild, closestInjector);
const guardVal = isCanActivateChild(guard) ?
guard.canActivateChild(futureARS, futureRSS) :
closestInjector.runInContext(() => guard(futureARS, futureRSS));
closestInjector.runInContext(
() => (guard as CanActivateChildFn)(futureARS, futureRSS));
return wrapIntoObservable(guardVal).pipe(first());
});
return of(guardsMapped).pipe(prioritizedGuardValue());
@ -161,8 +162,8 @@ function runCanDeactivate(
const guard = getTokenOrFunctionIdentity<any>(c, closestInjector);
const guardVal = isCanDeactivate(guard) ?
guard.canDeactivate(component, currARS, currRSS, futureRSS) :
closestInjector.runInContext<boolean|UrlTree>(
() => guard(component, currARS, currRSS, futureRSS));
closestInjector.runInContext(
() => (guard as CanDeactivateFn<any>)(component, currARS, currRSS, futureRSS));
return wrapIntoObservable(guardVal).pipe(first());
});
return of(canDeactivateObservables).pipe(prioritizedGuardValue());
@ -180,7 +181,7 @@ export function runCanLoadGuards(
const guard = getTokenOrFunctionIdentity<any>(injectionToken, injector);
const guardVal = isCanLoad(guard) ?
guard.canLoad(route, segments) :
injector.runInContext<boolean|UrlTree>(() => guard(route, segments));
injector.runInContext(() => (guard as CanLoadFn)(route, segments));
return wrapIntoObservable(guardVal);
});
@ -213,7 +214,7 @@ export function runCanMatchGuards(
const guard = getTokenOrFunctionIdentity(injectionToken, injector);
const guardVal = isCanMatch(guard) ?
guard.canMatch(route, segments) :
injector.runInContext<boolean|UrlTree>(() => guard(route, segments));
injector.runInContext(() => (guard as CanMatchFn)(route, segments));
return wrapIntoObservable(guardVal);
});