Skip to content

Commit

Permalink
feat(router): allow guards and resolvers to be plain functions
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()]}
```
  • Loading branch information
atscott committed Aug 2, 2022
1 parent 1fe759d commit 66cb5fd
Show file tree
Hide file tree
Showing 14 changed files with 493 additions and 187 deletions.
29 changes: 22 additions & 7 deletions goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export {
} from './render3/jit/pipe';
export {
isStandalone as ɵisStandalone,
isInjectable as ɵisInjectable,
} from './render3/jit/module';
export { Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent } from './render3/profiler';
export {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/render3/jit/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {getCompilerFacade, JitCompilerUsage, R3InjectorMetadataFacade} from '../../compiler/compiler_facade';
import {ProviderToken} from '../../di';
import {resolveForwardRef} from '../../di/forward_ref';
import {NG_INJ_DEF} from '../../di/interface/defs';
import {ModuleWithProviders} from '../../di/interface/provider';
Expand Down Expand Up @@ -202,6 +203,10 @@ export function isStandalone<T>(type: Type<T>) {
return def !== null ? def.standalone : false;
}

export function isInjectable(token: ProviderToken<unknown>) {
return (token as any).ɵprov !== undefined;
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@
"name": "getTView"
},
{
"name": "getToken"
"name": "getTokenOrFunctionIdentity"
},
{
"name": "getViewRefs"
Expand Down
2 changes: 1 addition & 1 deletion packages/router/src/index.ts
Original file line number Diff line number Diff line change
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
77 changes: 64 additions & 13 deletions packages/router/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {EnvironmentInjector, ImportedNgModuleProviders, InjectionToken, NgModuleFactory, Provider, Type} from '@angular/core';
import {EnvironmentInjector, ImportedNgModuleProviders, InjectionToken, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core';
import {Observable} from 'rxjs';

import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
Expand Down Expand Up @@ -85,7 +85,7 @@ export type Data = {
* @publicApi
*/
export type ResolveData = {
[key: string|symbol]: any
[key: string|symbol]: any|ResolveFn<unknown>
};

/**
Expand Down Expand Up @@ -395,7 +395,7 @@ export interface Route {
*
* @see `PageTitleStrategy`
*/
title?: string|Type<Resolve<string>>;
title?: string|Type<Resolve<string>>|ResolveFn<string>;

/**
* The path to match against. Cannot be used together with a custom `matcher` function.
Expand Down Expand Up @@ -460,36 +460,50 @@ export interface Route {
*/
outlet?: string;
/**
* An array of dependency-injection tokens used to look up `CanActivate()`
* An array of `CanActivateFn` or DI tokens used to look up `CanActivate()`
* handlers, in order to determine if the current user is allowed to
* activate the component. By default, any user can activate.
*
* 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?: any[];
canActivate?: Array<CanActivateFn|any>;
/**
* An array of DI tokens used to look up `CanMatch()`
* An array of `CanMatchFn` or DI tokens used to look up `CanMatch()`
* handlers, in order to determine if the current user is allowed to
* match the `Route`. By default, any route can match.
*
* 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>>;
canMatch?: Array<Type<CanMatch>|InjectionToken<CanMatchFn>|CanMatchFn>;
/**
* An array of DI tokens used to look up `CanActivateChild()` handlers,
* 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
* a child of the component. By default, any user can activate a child.
*
* 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?: any[];
canActivateChild?: Array<CanActivateChildFn|any>;
/**
* An array of DI tokens used to look up `CanDeactivate()`
* An array of `CanDeactivateFn` or DI tokens used to look up `CanDeactivate()`
* handlers, in order to determine if the current user is allowed to
* deactivate the component. By default, any user can deactivate.
*
* 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?: any[];
canDeactivate?: Array<CanDeactivateFn<any>|any>;
/**
* An array of DI tokens used to look up `CanLoad()`
* An array of `CanLoadFn` or DI tokens used to look up `CanLoad()`
* handlers, in order to determine if the current user is allowed to
* load the component. By default, any user can load.
*
* 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.
*/
canLoad?: any[];
canLoad?: Array<CanLoadFn|any>;
/**
* Additional developer-defined data provided to the component via
* `ActivatedRoute`. By default, no additional data is passed.
Expand Down Expand Up @@ -635,6 +649,13 @@ export interface CanActivate {
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
}

/**
* The signature of a function used as a `canActivate` guard on a `Route`.
*
* @publicApi
* @see `CanActivate`
* @see `Route`
*/
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;

Expand Down Expand Up @@ -729,6 +750,13 @@ export interface CanActivateChild {
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
}

/**
* The signature of a function used as a `canActivateChild` guard on a `Route`.
*
* @publicApi
* @see `CanActivateChild`
* @see `Route`
*/
export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;

Expand Down Expand Up @@ -819,6 +847,13 @@ export interface CanDeactivate<T> {
|UrlTree;
}

/**
* The signature of a function used as a `canDeactivate` guard on a `Route`.
*
* @publicApi
* @see `CanDeactivate`
* @see `Route`
*/
export type CanDeactivateFn<T> =
(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot) =>
Expand Down Expand Up @@ -1055,6 +1090,15 @@ export interface Resolve<T> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T>|Promise<T>|T;
}

/**
* Function type definition for a data provider.
*
* @see `Route#resolve`.
* @publicApi
*/
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
Observable<T>|Promise<T>|T;

/**
* @description
*
Expand Down Expand Up @@ -1137,6 +1181,13 @@ export interface CanLoad {
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;
}

/**
* The signature of a function used as a `canLoad` guard on a `Route`.
*
* @publicApi
* @see `CanLoad`
* @see `Route`
*/
export type CanLoadFn = (route: Route, segments: UrlSegment[]) =>
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree;

Expand Down

0 comments on commit 66cb5fd

Please sign in to comment.