From 926c35f4ac70f5e4d142e545d6d056dd67aac97b Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 31 Oct 2022 09:13:05 -0700 Subject: [PATCH] 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>): 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 --- aio/content/guide/deprecations.md | 3 ++ goldens/public-api/router/index.md | 26 ++++++----- packages/router/src/index.ts | 2 +- packages/router/src/models.ts | 43 +++++++++++++++---- packages/router/src/operators/check_guards.ts | 13 +++--- 5 files changed, 61 insertions(+), 26 deletions(-) diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index 718453baa71..6e4c47aad3e 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -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. | + diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index 11230f04e8f..452fca2902e 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -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 | Promise | boolean | UrlTree; } -// @public +// @public @deprecated export interface CanActivateChild { // (undocumented) canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree; @@ -129,7 +130,7 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou // @public export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable | Promise | boolean | UrlTree; -// @public +// @public @deprecated export interface CanDeactivate { // (undocumented) canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): Observable | Promise | boolean | UrlTree; @@ -147,7 +148,7 @@ export interface CanLoad { // @public @deprecated export type CanLoadFn = (route: Route, segments: UrlSegment[]) => Observable | Promise | boolean | UrlTree; -// @public +// @public @deprecated export interface CanMatch { // (undocumented) canMatch(route: Route, segments: UrlSegment[]): Observable | Promise | boolean | UrlTree; @@ -237,6 +238,9 @@ export class DefaultUrlSerializer implements UrlSerializer { serialize(tree: UrlTree): string; } +// @public @deprecated +export type DeprecatedGuard = ProviderToken | 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 { // (undocumented) resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | T; @@ -569,7 +573,7 @@ export interface Resolve { // @public export type ResolveData = { - [key: string | symbol]: any | ResolveFn; + [key: string | symbol]: ResolveFn | DeprecatedGuard; }; // @public @@ -611,12 +615,12 @@ export class ResolveStart extends RouterEvent { // @public export interface Route { - canActivate?: Array; - canActivateChild?: Array; - canDeactivate?: Array | any>; + canActivate?: Array; + canActivateChild?: Array; + canDeactivate?: Array | DeprecatedGuard>; // @deprecated - canLoad?: Array; - canMatch?: Array | InjectionToken | CanMatchFn>; + canLoad?: Array; + canMatch?: Array; children?: Routes; component?: Type; data?: Data; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 3490400bc5d..c0dc153765c 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -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'; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index 201fcdfff6f..4f40dac61bc 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -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; + /** * 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 + [key: string|symbol]: ResolveFn|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; + canActivate?: Array; /** * 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|InjectionToken|CanMatchFn>; + canMatch?: Array; /** * 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; + canActivateChild?: Array; /** * 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|any>; + canDeactivate?: Array|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; + canLoad?: Array; /** * 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 { canDeactivate( @@ -982,6 +1005,8 @@ export type CanDeactivateFn = * ``` * * @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 { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable|Promise|T; @@ -1196,7 +1223,7 @@ export type ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSna * ``` * * @publicApi - * @deprecated Use `CanMatch` instead + * @deprecated Use `CanMatchFn` instead */ export interface CanLoad { canLoad(route: Route, segments: UrlSegment[]): diff --git a/packages/router/src/operators/check_guards.ts b/packages/router/src/operators/check_guards.ts index d832c5aebf9..2de2159766e 100644 --- a/packages/router/src/operators/check_guards.ts +++ b/packages/router/src/operators/check_guards.ts @@ -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, 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(c, closestInjector); const guardVal = isCanDeactivate(guard) ? guard.canDeactivate(component, currARS, currRSS, futureRSS) : - closestInjector.runInContext( - () => guard(component, currARS, currRSS, futureRSS)); + closestInjector.runInContext( + () => (guard as CanDeactivateFn)(component, currARS, currRSS, futureRSS)); return wrapIntoObservable(guardVal).pipe(first()); }); return of(canDeactivateObservables).pipe(prioritizedGuardValue()); @@ -180,7 +181,7 @@ export function runCanLoadGuards( const guard = getTokenOrFunctionIdentity(injectionToken, injector); const guardVal = isCanLoad(guard) ? guard.canLoad(route, segments) : - injector.runInContext(() => 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(() => guard(route, segments)); + injector.runInContext(() => (guard as CanMatchFn)(route, segments)); return wrapIntoObservable(guardVal); });