Skip to content

Commit

Permalink
feat(router): allow guards and resolvers to be plain functions (#46684)
Browse files Browse the repository at this point in the history
The current Router APIs require guards/resolvers to be present in the DI tree. This is because we want to treat all guards/resolvers equally and some may require dependencies. This requirement results in quite a lot of boilerplate for guards. Here are two examples:

```
const MY_GUARD = new InjectionToken<any>('my_guard');
…
providers: {provide: MY_GUARD, useValue: () => window.someGlobalState}
…
const route = {path: 'somePath', canActivate: [MY_GUARD]}
```

```
@Injectable({providedIn: 'root'})
export class MyGuardWithDependency {
  constructor(private myDep: MyDependency) {}

  canActivate() {
    return myDep.canActivate();
  }
}
…
const route = {path: 'somePath', canActivate: [MyGuardWithDependency]}
```

Notice that even when we want to write a simple guard that has no dependencies as in the first example, we still have to write either an InjectionToken or an Injectable class.

With this commit router guards and resolvers can be plain old functions.
 For example:

```
const route = {path: 'somePath', component: EditCmp, canDeactivate: [(component: EditCmp) => !component.hasUnsavedChanges]}
```

Additionally, these functions can still use Angular DI with `inject` from `@angular/core`.

```
const route = {path: 'somePath', canActivate: [() => inject(MyDependency).canActivate()]}
```

PR Close #46684
  • Loading branch information
atscott committed Aug 5, 2022
1 parent 0920a15 commit 0abb67a
Show file tree
Hide file tree
Showing 18 changed files with 527 additions and 258 deletions.
29 changes: 22 additions & 7 deletions goldens/public-api/router/index.md
Expand Up @@ -125,18 +125,30 @@ export interface CanActivateChild {
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}

// @public
export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;

// @public
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;

// @public
export interface CanDeactivate<T> {
// (undocumented)
canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}

// @public
export type CanDeactivateFn<T> = (component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;

// @public
export interface CanLoad {
// (undocumented)
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}

// @public
export type CanLoadFn = (route: Route, segments: UrlSegment[]) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;

// @public
export interface CanMatch {
// (undocumented)
Expand Down Expand Up @@ -505,7 +517,7 @@ export interface Resolve<T> {

// @public
export type ResolveData = {
[key: string | symbol]: any;
[key: string | symbol]: any | ResolveFn<unknown>;
};

// @public
Expand All @@ -525,6 +537,9 @@ export class ResolveEnd extends RouterEvent {
urlAfterRedirects: string;
}

// @public
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<T> | Promise<T> | T;

// @public
export class ResolveStart extends RouterEvent {
constructor(
Expand All @@ -544,11 +559,11 @@ export class ResolveStart extends RouterEvent {

// @public
export interface Route {
canActivate?: any[];
canActivateChild?: any[];
canDeactivate?: any[];
canLoad?: any[];
canMatch?: Array<Type<CanMatch> | InjectionToken<CanMatchFn>>;
canActivate?: Array<CanActivateFn | any>;
canActivateChild?: Array<CanActivateChildFn | any>;
canDeactivate?: Array<CanDeactivateFn<any> | any>;
canLoad?: Array<CanLoadFn | any>;
canMatch?: Array<Type<CanMatch> | InjectionToken<CanMatchFn> | CanMatchFn>;
children?: Routes;
component?: Type<any>;
data?: Data;
Expand All @@ -562,7 +577,7 @@ export interface Route {
redirectTo?: string;
resolve?: ResolveData;
runGuardsAndResolvers?: RunGuardsAndResolvers;
title?: string | Type<Resolve<string>>;
title?: string | Type<Resolve<string>> | ResolveFn<string>;
}

// @public
Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Expand Up @@ -33,7 +33,7 @@
"cli-hello-world-lazy": {
"uncompressed": {
"runtime": 2835,
"main": 238214,
"main": 238737,
"polyfills": 33842,
"src_app_lazy_lazy_module_ts": 780
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/application_ref.ts
Expand Up @@ -34,10 +34,10 @@ import {InternalViewRef, ViewRef} from './linker/view_ref';
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading';
import {assertNgModuleType} from './render3/assert';
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
import {isStandalone} from './render3/definition';
import {assertStandaloneComponentType} from './render3/errors';
import {setLocaleId} from './render3/i18n/i18n_locale_id';
import {setJitOptions} from './render3/jit/jit_options';
import {isStandalone} from './render3/jit/module';
import {createEnvironmentInjector, NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
import {TESTABILITY} from './testability/testability';
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/core_render3_private_export.ts
Expand Up @@ -24,6 +24,7 @@ export {
export {
NG_INJ_DEF as ɵNG_INJ_DEF,
NG_PROV_DEF as ɵNG_PROV_DEF,
isInjectable as ɵisInjectable,
} from './di/interface/defs';
export {createInjector as ɵcreateInjector} from './di/create_injector';
export {
Expand Down Expand Up @@ -252,9 +253,7 @@ export {
export {
compilePipe as ɵcompilePipe,
} from './render3/jit/pipe';
export {
isStandalone as ɵisStandalone,
} from './render3/jit/module';
export { isStandalone as ɵisStandalone} from './render3/definition';
export { Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent } from './render3/profiler';
export {
publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/di/interface/defs.ts
Expand Up @@ -190,6 +190,10 @@ export function getInjectableDef<T>(type: any): ɵɵInjectableDeclaration<T>|nul
return getOwnDefinition(type, NG_PROV_DEF) || getOwnDefinition(type, NG_INJECTABLE_DEF);
}

export function isInjectable(type: any): boolean {
return getInjectableDef(type) !== null;
}

/**
* Return definition only if it is defined directly on `type` and is not inherited from a base
* class of `type`.
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/render3/definition.ts
Expand Up @@ -7,6 +7,7 @@
*/

import {ChangeDetectionStrategy} from '../change_detection/constants';
import {NG_PROV_DEF} from '../di/interface/defs';
import {Mutable, Type} from '../interface/type';
import {NgModuleDef, NgModuleType} from '../metadata/ng_module_def';
import {SchemaMetadata} from '../metadata/schema';
Expand Down Expand Up @@ -743,6 +744,11 @@ export function getPipeDef<T>(type: any): PipeDef<T>|null {
return type[NG_PIPE_DEF] || null;
}

export function isStandalone<T>(type: Type<T>): boolean {
const def = getComponentDef(type) || getDirectiveDef(type) || getPipeDef(type);
return def !== null ? def.standalone : false;
}

export function getNgModuleDef<T>(type: any, throwNotFound: true): NgModuleDef<T>;
export function getNgModuleDef<T>(type: any): NgModuleDef<T>|null;
export function getNgModuleDef<T>(type: any, throwNotFound?: boolean): NgModuleDef<T>|null {
Expand Down
7 changes: 1 addition & 6 deletions packages/core/src/render3/jit/module.ts
Expand Up @@ -19,7 +19,7 @@ import {NgModuleDef, NgModuleTransitiveScopes, NgModuleType} from '../../metadat
import {deepForEach, flatten} from '../../util/array_utils';
import {assertDefined} from '../../util/assert';
import {EMPTY_ARRAY} from '../../util/empty';
import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef} from '../definition';
import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef, isStandalone} from '../definition';
import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF, NG_MOD_DEF, NG_PIPE_DEF} from '../fields';
import {ComponentDef} from '../interfaces/definition';
import {maybeUnwrapFn} from '../util/misc_utils';
Expand Down Expand Up @@ -197,11 +197,6 @@ export function compileNgModuleDefs(
});
}

export function isStandalone<T>(type: Type<T>) {
const def = getComponentDef(type) || getDirectiveDef(type) || getPipeDef(type);
return def !== null ? def.standalone : false;
}

export function generateStandaloneInDeclarationsError(type: Type<any>, location: string) {
const prefix = `Unexpected "${stringifyForError(type)}" found in the "declarations" array of the`;
const suffix = `"${stringifyForError(type)}" is marked as standalone and can't be declared ` +
Expand Down
Expand Up @@ -1314,7 +1314,7 @@
"name": "getTView"
},
{
"name": "getToken"
"name": "getTokenOrFunctionIdentity"
},
{
"name": "getViewRefs"
Expand Down
2 changes: 1 addition & 1 deletion packages/router/src/index.ts
Expand Up @@ -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, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, CanMatchFn, Data, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, QueryParamsHandling, Resolve, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {CanActivate, CanActivateChild, CanActivateChildFn, CanActivateFn, CanDeactivate, CanDeactivateFn, CanLoad, CanLoadFn, CanMatch, CanMatchFn, Data, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, QueryParamsHandling, Resolve, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models';
export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy';
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {Navigation, NavigationExtras, Router, UrlCreationOptions} from './router';
Expand Down

0 comments on commit 0abb67a

Please sign in to comment.