From 1a1dd53cdc8e55e49e1b64234260b5c92051e9f4 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 1 Nov 2022 13:19:42 -0700 Subject: [PATCH 1/2] feat(@schematics/angular): Add schematics for generating functional router guards and resolvers Functional guards and resolvers were introduced in the Angular router in v14.2. This commit adds the ability to generate functional router guards by specifying `--guardType` instead of `--implements`. These guards are also accompanied by a test that includes a helper function for executing the guard in the `TestBed` environment so that any `inject` calls in the guard will work properly. Functional resolvers are generated by adding the `--functional` flag. --- packages/schematics/angular/BUILD.bazel | 8 +++ .../__name@dasherize__.guard.spec.ts.template | 0 .../__name@dasherize__.guard.ts.template | 2 +- packages/schematics/angular/guard/index.ts | 59 ++++++++++------- .../schematics/angular/guard/index_spec.ts | 63 +++++++++++++++---- packages/schematics/angular/guard/schema.json | 9 ++- .../__name@dasherize__.guard.spec.ts.template | 18 ++++++ .../__name@dasherize__.guard.ts.template | 9 +++ ...name@dasherize__.resolver.spec.ts.template | 0 .../__name@dasherize__.resolver.ts.template | 0 ...name@dasherize__.resolver.spec.ts.template | 17 +++++ .../__name@dasherize__.resolver.ts.template | 5 ++ packages/schematics/angular/resolver/index.ts | 4 +- .../schematics/angular/resolver/index_spec.ts | 23 +++++++ .../schematics/angular/resolver/schema.json | 5 ++ .../angular/utility/generate-from-files.ts | 4 +- 16 files changed, 186 insertions(+), 40 deletions(-) rename packages/schematics/angular/guard/{files => implements-files}/__name@dasherize__.guard.spec.ts.template (100%) rename packages/schematics/angular/guard/{files => implements-files}/__name@dasherize__.guard.ts.template (95%) create mode 100644 packages/schematics/angular/guard/type-files/__name@dasherize__.guard.spec.ts.template create mode 100644 packages/schematics/angular/guard/type-files/__name@dasherize__.guard.ts.template rename packages/schematics/angular/resolver/{files => class-files}/__name@dasherize__.resolver.spec.ts.template (100%) rename packages/schematics/angular/resolver/{files => class-files}/__name@dasherize__.resolver.ts.template (100%) create mode 100644 packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.spec.ts.template create mode 100644 packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.ts.template diff --git a/packages/schematics/angular/BUILD.bazel b/packages/schematics/angular/BUILD.bazel index 8210ad2207ab..83da30a8bacc 100644 --- a/packages/schematics/angular/BUILD.bazel +++ b/packages/schematics/angular/BUILD.bazel @@ -47,6 +47,10 @@ ts_library( # Also exclude templated files. "*/files/**/*.ts", "*/other-files/**/*.ts", + "*/implements-files/**/*", + "*/type-files/**/*", + "*/functional-files/**/*", + "*/class-files/**/*", # Exclude test helpers. "utility/test/**/*.ts", # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces @@ -65,6 +69,10 @@ ts_library( "*/schema.json", "*/files/**/*", "*/other-files/**/*", + "*/implements-files/**/*", + "*/type-files/**/*", + "*/functional-files/**/*", + "*/class-files/**/*", ], exclude = [ # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces diff --git a/packages/schematics/angular/guard/files/__name@dasherize__.guard.spec.ts.template b/packages/schematics/angular/guard/implements-files/__name@dasherize__.guard.spec.ts.template similarity index 100% rename from packages/schematics/angular/guard/files/__name@dasherize__.guard.spec.ts.template rename to packages/schematics/angular/guard/implements-files/__name@dasherize__.guard.spec.ts.template diff --git a/packages/schematics/angular/guard/files/__name@dasherize__.guard.ts.template b/packages/schematics/angular/guard/implements-files/__name@dasherize__.guard.ts.template similarity index 95% rename from packages/schematics/angular/guard/files/__name@dasherize__.guard.ts.template rename to packages/schematics/angular/guard/implements-files/__name@dasherize__.guard.ts.template index 8d83bc7498b4..5f9dac95d0b4 100644 --- a/packages/schematics/angular/guard/files/__name@dasherize__.guard.ts.template +++ b/packages/schematics/angular/guard/implements-files/__name@dasherize__.guard.ts.template @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { <%= implementationImports %> } from '@angular/router'; +import { <%= routerImports %> } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ diff --git a/packages/schematics/angular/guard/index.ts b/packages/schematics/angular/guard/index.ts index efb377216684..f5b4e5fea664 100644 --- a/packages/schematics/angular/guard/index.ts +++ b/packages/schematics/angular/guard/index.ts @@ -7,39 +7,54 @@ */ import { Rule, SchematicsException } from '@angular-devkit/schematics'; + import { generateFromFiles } from '../utility/generate-from-files'; + import { Implement as GuardInterface, Schema as GuardOptions } from './schema'; export default function (options: GuardOptions): Rule { - if (!options.implements) { - throw new SchematicsException('Option "implements" is required.'); + if (options.implements && options.implements.length > 0 && options.guardType) { + throw new SchematicsException('Options "implements" and "guardType" cannot be used together.'); } - const implementations = options.implements - .map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate' : implement)) - .join(', '); - const commonRouterNameImports = ['ActivatedRouteSnapshot', 'RouterStateSnapshot']; - const routerNamedImports: string[] = [...options.implements, 'UrlTree']; + if (options.guardType) { + const guardType = options.guardType.replace(/^can/, 'Can') + 'Fn'; - if ( - options.implements.includes(GuardInterface.CanLoad) || - options.implements.includes(GuardInterface.CanMatch) - ) { - routerNamedImports.push('Route', 'UrlSegment'); + return generateFromFiles({ ...options, templateFilesDirectory: './type-files' }, { guardType }); + } else { + if (!options.implements || options.implements.length < 1) { + options.implements = [GuardInterface.CanActivate]; + } - if (options.implements.length > 1) { + const implementations = options.implements + .map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate' : implement)) + .join(', '); + const commonRouterNameImports = ['ActivatedRouteSnapshot', 'RouterStateSnapshot']; + const routerNamedImports: string[] = [...options.implements, 'UrlTree']; + + if ( + options.implements.includes(GuardInterface.CanLoad) || + options.implements.includes(GuardInterface.CanMatch) + ) { + routerNamedImports.push('Route', 'UrlSegment'); + + if (options.implements.length > 1) { + routerNamedImports.push(...commonRouterNameImports); + } + } else { routerNamedImports.push(...commonRouterNameImports); } - } else { - routerNamedImports.push(...commonRouterNameImports); - } - routerNamedImports.sort(); + routerNamedImports.sort(); - const implementationImports = routerNamedImports.join(', '); + const routerImports = routerNamedImports.join(', '); - return generateFromFiles(options, { - implementations, - implementationImports, - }); + return generateFromFiles( + { ...options, templateFilesDirectory: './implements-files' }, + { + implementations, + routerImports, + }, + ); + } } diff --git a/packages/schematics/angular/guard/index_spec.ts b/packages/schematics/angular/guard/index_spec.ts index 45326eba1862..fdb136701891 100644 --- a/packages/schematics/angular/guard/index_spec.ts +++ b/packages/schematics/angular/guard/index_spec.ts @@ -7,8 +7,10 @@ */ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; + import { Schema as ApplicationOptions } from '../application/schema'; import { Schema as WorkspaceOptions } from '../workspace/schema'; + import { Schema as GuardOptions } from './schema'; describe('Guard Schematic', () => { @@ -90,6 +92,37 @@ describe('Guard Schematic', () => { expect(fileString).not.toContain('canLoad'); }); + it('should respect the guardType value', async () => { + const options = { ...defaultOptions, guardType: 'canActivate' }; + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); + expect(fileString).toContain('export const fooGuard: CanActivateFn = (route, state) => {'); + expect(fileString).not.toContain('CanActivateChild'); + expect(fileString).not.toContain('canActivateChild'); + expect(fileString).not.toContain('CanLoad'); + expect(fileString).not.toContain('canLoad'); + }); + + it('should generate a helper function to execute the guard in a test', async () => { + const options = { ...defaultOptions, guardType: 'canActivate' }; + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.guard.spec.ts'); + expect(fileString).toContain('const executeGuard: CanActivateFn = (...guardParameters) => '); + expect(fileString).toContain( + 'TestBed.inject(EnvironmentInjector).runInContext(() => fooGuard(...guardParameters));', + ); + }); + + it('should generate CanDeactivateFn with unknown guardType', async () => { + const options = { ...defaultOptions, guardType: 'canDeactivate' }; + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); + expect(fileString).toContain( + 'export const fooGuard: CanDeactivateFn = ' + + '(component, currentRoute, currentState, nextState) => {', + ); + }); + it('should respect the implements values', async () => { const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild']; const options = { ...defaultOptions, implements: implementationOptions }; @@ -104,18 +137,6 @@ describe('Guard Schematic', () => { }); }); - it('should use CanActivate if no implements value', async () => { - const options = { ...defaultOptions, implements: undefined }; - const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); - const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); - expect(fileString).toContain('CanActivate'); - expect(fileString).toContain('canActivate'); - expect(fileString).not.toContain('CanActivateChild'); - expect(fileString).not.toContain('canActivateChild'); - expect(fileString).not.toContain('CanLoad'); - expect(fileString).not.toContain('canLoad'); - }); - it('should add correct imports based on CanLoad implementation', async () => { const implementationOptions = ['CanLoad']; const options = { ...defaultOptions, implements: implementationOptions }; @@ -136,6 +157,15 @@ describe('Guard Schematic', () => { expect(fileString).toContain(expectedImports); }); + it('should add correct imports based on canLoad guardType', async () => { + const options = { ...defaultOptions, guardType: 'canLoad' }; + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); + const expectedImports = `import { CanLoadFn } from '@angular/router';`; + + expect(fileString).toContain(expectedImports); + }); + it('should add correct imports based on CanActivate implementation', async () => { const implementationOptions = ['CanActivate']; const options = { ...defaultOptions, implements: implementationOptions }; @@ -146,6 +176,15 @@ describe('Guard Schematic', () => { expect(fileString).toContain(expectedImports); }); + it('should add correct imports based on canActivate guardType', async () => { + const options = { ...defaultOptions, guardType: 'canActivate' }; + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); + const expectedImports = `import { CanActivateFn } from '@angular/router';`; + + expect(fileString).toContain(expectedImports); + }); + it('should add correct imports if multiple implementations was selected', async () => { const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild']; const options = { ...defaultOptions, implements: implementationOptions }; diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index d49d12778803..f25a55f17437 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -49,9 +49,12 @@ "items": { "enum": ["CanActivate", "CanActivateChild", "CanDeactivate", "CanLoad", "CanMatch"], "type": "string" - }, - "default": ["CanActivate"], - "x-prompt": "Which interfaces would you like to implement?" + } + }, + "guardType": { + "type": "string", + "description": "Specifies type of guard to generate.", + "enum": ["canActivate", "canActivateChild", "canDeactivate", "canLoad", "canMatch"] } }, "required": ["name", "project"] diff --git a/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.spec.ts.template b/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.spec.ts.template new file mode 100644 index 000000000000..4cbcd9b606d6 --- /dev/null +++ b/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.spec.ts.template @@ -0,0 +1,18 @@ +import { EnvironmentInjector } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { <%= guardType %> } from '@angular/router'; + +import { <%= camelize(name) %>Guard } from './<%= dasherize(name) %>.guard'; + +describe('<%= camelize(name) %>Guard', () => { + const executeGuard: <%= guardType %> = (...guardParameters) => + TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Guard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(<%= camelize(name) %>Guard).toBeTruthy(); + }); +}); diff --git a/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.ts.template b/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.ts.template new file mode 100644 index 000000000000..9f82b7681ecd --- /dev/null +++ b/packages/schematics/angular/guard/type-files/__name@dasherize__.guard.ts.template @@ -0,0 +1,9 @@ +import { <%= guardType %> } from '@angular/router'; + +export const <%= camelize(name) %>Guard: <%= guardType %><% if (guardType === 'CanDeactivateFn') { %><% } %> = <% + if (guardType === 'CanMatchFn' || guardType === 'CanLoadFn') { %>(route, segments)<% } + %><% if (guardType === 'CanActivateFn') { %>(route, state)<% } + %><% if (guardType === 'CanActivateChildFn') { %>(childRoute, state)<% } + %><% if (guardType === 'CanDeactivateFn') { %>(component, currentRoute, currentState, nextState)<% } %> => { + return true; +} diff --git a/packages/schematics/angular/resolver/files/__name@dasherize__.resolver.spec.ts.template b/packages/schematics/angular/resolver/class-files/__name@dasherize__.resolver.spec.ts.template similarity index 100% rename from packages/schematics/angular/resolver/files/__name@dasherize__.resolver.spec.ts.template rename to packages/schematics/angular/resolver/class-files/__name@dasherize__.resolver.spec.ts.template diff --git a/packages/schematics/angular/resolver/files/__name@dasherize__.resolver.ts.template b/packages/schematics/angular/resolver/class-files/__name@dasherize__.resolver.ts.template similarity index 100% rename from packages/schematics/angular/resolver/files/__name@dasherize__.resolver.ts.template rename to packages/schematics/angular/resolver/class-files/__name@dasherize__.resolver.ts.template diff --git a/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.spec.ts.template b/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.spec.ts.template new file mode 100644 index 000000000000..9b521b298d4d --- /dev/null +++ b/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.spec.ts.template @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { ResolveFn } from '@angular/router'; + +import { <%= camelize(name) %>Resolver } from './<%= dasherize(name) %>.resolver'; + +describe('<%= camelize(name) %>Resolver', () => { + const executeResolver: ResolveFn = (...resolverParameters) => + TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Resolver(...resolverParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); +}); diff --git a/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.ts.template b/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.ts.template new file mode 100644 index 000000000000..64ceec5c4272 --- /dev/null +++ b/packages/schematics/angular/resolver/functional-files/__name@dasherize__.resolver.ts.template @@ -0,0 +1,5 @@ +import { ResolveFn } from '@angular/router'; + +export const <%= camelize(name) %>Resolver: ResolveFn = (route, state) => { + return true; +} diff --git a/packages/schematics/angular/resolver/index.ts b/packages/schematics/angular/resolver/index.ts index 35ae7dd27503..2a08b956cae8 100644 --- a/packages/schematics/angular/resolver/index.ts +++ b/packages/schematics/angular/resolver/index.ts @@ -11,5 +11,7 @@ import { generateFromFiles } from '../utility/generate-from-files'; import { Schema } from './schema'; export default function (options: Schema): Rule { - return generateFromFiles(options); + return options.functional + ? generateFromFiles({ ...options, templateFilesDirectory: './functional-files' }) + : generateFromFiles({ ...options, templateFilesDirectory: './class-files' }); } diff --git a/packages/schematics/angular/resolver/index_spec.ts b/packages/schematics/angular/resolver/index_spec.ts index af2fdd22087e..95f1bdde8305 100644 --- a/packages/schematics/angular/resolver/index_spec.ts +++ b/packages/schematics/angular/resolver/index_spec.ts @@ -79,4 +79,27 @@ describe('resolver Schematic', () => { .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo.resolver.ts'); }); + + it('should create a functional resolver', async () => { + const tree = await schematicRunner + .runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree) + .toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.ts'); + expect(fileString).toContain( + 'export const fooResolver: ResolveFn = (route, state) => {', + ); + }); + + it('should create a helper function to run a functional resolver in a test', async () => { + const tree = await schematicRunner + .runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree) + .toPromise(); + const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.spec.ts'); + expect(fileString).toContain( + 'const executeResolver: ResolveFn = (...resolverParameters) => ', + ); + expect(fileString).toContain( + 'TestBed.inject(EnvironmentInjector).runInContext(() => fooResolver(...resolverParameters));', + ); + }); }); diff --git a/packages/schematics/angular/resolver/schema.json b/packages/schematics/angular/resolver/schema.json index 72b5620630c1..f6f3d97987ee 100644 --- a/packages/schematics/angular/resolver/schema.json +++ b/packages/schematics/angular/resolver/schema.json @@ -25,6 +25,11 @@ "description": "When true (the default), creates the new files at the top level of the current project.", "default": true }, + "functional": { + "type": "boolean", + "description": "Creates the resolver as a `ResolveFn`.", + "default": false + }, "path": { "type": "string", "format": "path", diff --git a/packages/schematics/angular/utility/generate-from-files.ts b/packages/schematics/angular/utility/generate-from-files.ts index eaf8febb54e5..a4e34b6bd188 100644 --- a/packages/schematics/angular/utility/generate-from-files.ts +++ b/packages/schematics/angular/utility/generate-from-files.ts @@ -30,6 +30,7 @@ export interface GenerateFromFilesOptions { prefix?: string; project: string; skipTests?: boolean; + templateFilesDirectory?: string; } export function generateFromFiles( @@ -47,7 +48,8 @@ export function generateFromFiles( validateClassName(strings.classify(options.name)); - const templateSource = apply(url('./files'), [ + const templateFilesDirectory = options.templateFilesDirectory ?? './files'; + const templateSource = apply(url(templateFilesDirectory), [ options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(), applyTemplates({ ...strings, From c33b5a85a041769f0357646ffa3fbce5b7345926 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 8 Nov 2022 10:58:21 -0800 Subject: [PATCH 2/2] fixup! feat(@schematics/angular): Add schematics for generating functional router guards and resolvers --- packages/schematics/angular/guard/index.ts | 17 +++++++++-------- .../schematics/angular/guard/index_spec.ts | 18 +++++++++--------- packages/schematics/angular/guard/schema.json | 16 +++++++++------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/schematics/angular/guard/index.ts b/packages/schematics/angular/guard/index.ts index f5b4e5fea664..a0fdc3c3e18d 100644 --- a/packages/schematics/angular/guard/index.ts +++ b/packages/schematics/angular/guard/index.ts @@ -13,19 +13,20 @@ import { generateFromFiles } from '../utility/generate-from-files'; import { Implement as GuardInterface, Schema as GuardOptions } from './schema'; export default function (options: GuardOptions): Rule { - if (options.implements && options.implements.length > 0 && options.guardType) { - throw new SchematicsException('Options "implements" and "guardType" cannot be used together.'); + if (!options.implements) { + throw new SchematicsException('Option "implements" is required.'); + } + if (options.implements.length > 1 && options.functional) { + throw new SchematicsException( + 'Can only specify one value for implements when generating a functional guard.', + ); } - if (options.guardType) { - const guardType = options.guardType.replace(/^can/, 'Can') + 'Fn'; + if (options.functional) { + const guardType = options.implements[0] + 'Fn'; return generateFromFiles({ ...options, templateFilesDirectory: './type-files' }, { guardType }); } else { - if (!options.implements || options.implements.length < 1) { - options.implements = [GuardInterface.CanActivate]; - } - const implementations = options.implements .map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate' : implement)) .join(', '); diff --git a/packages/schematics/angular/guard/index_spec.ts b/packages/schematics/angular/guard/index_spec.ts index fdb136701891..ebf85dd23529 100644 --- a/packages/schematics/angular/guard/index_spec.ts +++ b/packages/schematics/angular/guard/index_spec.ts @@ -92,8 +92,8 @@ describe('Guard Schematic', () => { expect(fileString).not.toContain('canLoad'); }); - it('should respect the guardType value', async () => { - const options = { ...defaultOptions, guardType: 'canActivate' }; + it('should respect the functional guard value', async () => { + const options = { ...defaultOptions, implements: ['CanActivate'], functional: true }; const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); expect(fileString).toContain('export const fooGuard: CanActivateFn = (route, state) => {'); @@ -104,7 +104,7 @@ describe('Guard Schematic', () => { }); it('should generate a helper function to execute the guard in a test', async () => { - const options = { ...defaultOptions, guardType: 'canActivate' }; + const options = { ...defaultOptions, implements: ['CanActivate'], functional: true }; const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.spec.ts'); expect(fileString).toContain('const executeGuard: CanActivateFn = (...guardParameters) => '); @@ -113,8 +113,8 @@ describe('Guard Schematic', () => { ); }); - it('should generate CanDeactivateFn with unknown guardType', async () => { - const options = { ...defaultOptions, guardType: 'canDeactivate' }; + it('should generate CanDeactivateFn with unknown functional guard', async () => { + const options = { ...defaultOptions, implements: ['CanDeactivate'], functional: true }; const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); expect(fileString).toContain( @@ -157,8 +157,8 @@ describe('Guard Schematic', () => { expect(fileString).toContain(expectedImports); }); - it('should add correct imports based on canLoad guardType', async () => { - const options = { ...defaultOptions, guardType: 'canLoad' }; + it('should add correct imports based on canLoad functional guard', async () => { + const options = { ...defaultOptions, implements: ['CanLoad'], functional: true }; const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); const expectedImports = `import { CanLoadFn } from '@angular/router';`; @@ -176,8 +176,8 @@ describe('Guard Schematic', () => { expect(fileString).toContain(expectedImports); }); - it('should add correct imports based on canActivate guardType', async () => { - const options = { ...defaultOptions, guardType: 'canActivate' }; + it('should add correct imports based on canActivate functional guard', async () => { + const options = { ...defaultOptions, implements: ['CanActivate'], functional: true }; const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); const expectedImports = `import { CanActivateFn } from '@angular/router';`; diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index f25a55f17437..d2df59cb0446 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -41,20 +41,22 @@ "$source": "projectName" } }, + "functional": { + "type": "boolean", + "description": "Specifies whether to generate a guard as a function.", + "default": false + }, "implements": { "type": "array", - "description": "Specifies which interfaces to implement.", + "description": "Specifies which type of guard to create.", "uniqueItems": true, "minItems": 1, "items": { "enum": ["CanActivate", "CanActivateChild", "CanDeactivate", "CanLoad", "CanMatch"], "type": "string" - } - }, - "guardType": { - "type": "string", - "description": "Specifies type of guard to generate.", - "enum": ["canActivate", "canActivateChild", "canDeactivate", "canLoad", "canMatch"] + }, + "default": ["CanActivate"], + "x-prompt": "Which type of guard would you like to create?" } }, "required": ["name", "project"]