Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8d715fa

Browse files
committed
fix(@schematics/angular): generate directives without a .directive extension/type
To align with the updated style guide, Angular v20 will generate services without a `.directive` file extension type for all directive related files by default. Projects will automatically use this naming convention. Projects can however opt-out by setting the `type` option to `Directive` for the directive schematic. This can be done as a default in the `angular.json` or directly on the commandline via `--type=Directive` when executing `ng generate`. As an example, `example.directive.ts` will now be named `example.ts`. Additionally, the TypeScript class name will be `Example` instead of the previous `ExampleDirective`.
1 parent b96dd54 commit 8d715fa

File tree

8 files changed

+61
-65
lines changed

8 files changed

+61
-65
lines changed

‎packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template‎

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { <%= classify(name) %><%= classify(type) %> } from './<%= dasherize(name) %><%= type ? '.' + dasherize(type) : '' %>';
2+
3+
describe('<%= classify(name) %><%= classify(type) %>', () => {
4+
it('should create an instance', () => {
5+
const directive = new <%= classify(name) %><%= classify(type) %>();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Directive } from '@angular/core';
44
selector: '[<%= selector %>]'<% if(!standalone) {%>,
55
standalone: false<%}%>
66
})
7-
export class <%= classify(name) %>Directive {
7+
export class <%= classify(name) %><%= classify(type) %> {
88

99
constructor() { }
1010

‎packages/schematics/angular/directive/index.ts‎

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Rule,
11-
SchematicsException,
12-
Tree,
13-
apply,
14-
applyTemplates,
15-
chain,
16-
filter,
17-
mergeWith,
18-
move,
19-
noop,
20-
strings,
21-
url,
22-
} from '@angular-devkit/schematics';
9+
import { Rule, SchematicsException, Tree, chain, strings } from '@angular-devkit/schematics';
2310
import { addDeclarationToNgModule } from '../utility/add-declaration-to-ng-module';
2411
import { findModuleFromOptions } from '../utility/find-module';
12+
import { generateFromFiles } from '../utility/generate-from-files';
2513
import { parseName } from '../utility/parse-name';
2614
import { validateClassName, validateHtmlSelector } from '../utility/validation';
2715
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
@@ -52,6 +40,9 @@ export default function (options: DirectiveOptions): Rule {
5240

5341
options.module = findModuleFromOptions(host, options);
5442

43+
// Schematic templates require a defined type value
44+
options.type ??= '';
45+
5546
const parsedPath = parseName(options.path, options.name);
5647
options.name = parsedPath.name;
5748
options.path = parsedPath.path;
@@ -60,23 +51,13 @@ export default function (options: DirectiveOptions): Rule {
6051
validateHtmlSelector(options.selector);
6152
validateClassName(strings.classify(options.name));
6253

63-
const templateSource = apply(url('./files'), [
64-
options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(),
65-
applyTemplates({
66-
...strings,
67-
'if-flat': (s: string) => (options.flat ? '' : s),
68-
...options,
69-
}),
70-
move(parsedPath.path),
71-
]);
72-
7354
return chain([
7455
addDeclarationToNgModule({
7556
type: 'directive',
7657

7758
...options,
7859
}),
79-
mergeWith(templateSource),
60+
generateFromFiles(options),
8061
]);
8162
};
8263
}

‎packages/schematics/angular/directive/index_spec.ts‎

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,47 +50,47 @@ describe('Directive Schematic', () => {
5050

5151
const tree = await schematicRunner.runSchematic('directive', options, appTree);
5252
const files = tree.files;
53-
expect(files).toContain('/projects/bar/src/app/foo/foo.directive.spec.ts');
54-
expect(files).toContain('/projects/bar/src/app/foo/foo.directive.ts');
53+
expect(files).toContain('/projects/bar/src/app/foo/foo.spec.ts');
54+
expect(files).toContain('/projects/bar/src/app/foo/foo.ts');
5555
});
5656

5757
it('should converts dash-cased-name to a camelCasedSelector', async () => {
5858
const options = { ...defaultOptions, name: 'my-dir' };
5959

6060
const tree = await schematicRunner.runSchematic('directive', options, appTree);
61-
const content = tree.readContent('/projects/bar/src/app/my-dir.directive.ts');
61+
const content = tree.readContent('/projects/bar/src/app/my-dir.ts');
6262
expect(content).toMatch(/selector:'\[appMyDir\]'/);
6363
});
6464

6565
it('should create the right selector with a path in the name', async () => {
6666
const options = { ...defaultOptions, name: 'sub/test' };
6767
appTree = await schematicRunner.runSchematic('directive', options, appTree);
6868

69-
const content = appTree.readContent('/projects/bar/src/app/sub/test.directive.ts');
69+
const content = appTree.readContent('/projects/bar/src/app/sub/test.ts');
7070
expect(content).toMatch(/selector:'\[appTest\]'/);
7171
});
7272

7373
it('should use the prefix', async () => {
7474
const options = { ...defaultOptions, prefix: 'pre' };
7575
const tree = await schematicRunner.runSchematic('directive', options, appTree);
7676

77-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
77+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
7878
expect(content).toMatch(/selector:'\[preFoo\]'/);
7979
});
8080

8181
it('should use the default project prefix if none is passed', async () => {
8282
const options = { ...defaultOptions, prefix: undefined };
8383
const tree = await schematicRunner.runSchematic('directive', options, appTree);
8484

85-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
85+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
8686
expect(content).toMatch(/selector:'\[appFoo\]'/);
8787
});
8888

8989
it('should use the supplied prefix if it is ""', async () => {
9090
const options = { ...defaultOptions, prefix: '' };
9191
const tree = await schematicRunner.runSchematic('directive', options, appTree);
9292

93-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
93+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
9494
expect(content).toMatch(/selector:'\[foo\]'/);
9595
});
9696

@@ -99,16 +99,16 @@ describe('Directive Schematic', () => {
9999

100100
const tree = await schematicRunner.runSchematic('directive', options, appTree);
101101
const files = tree.files;
102-
expect(files).toContain('/projects/bar/src/app/foo.directive.ts');
103-
expect(files).not.toContain('/projects/bar/src/app/foo.directive.spec.ts');
102+
expect(files).toContain('/projects/bar/src/app/foo.ts');
103+
expect(files).not.toContain('/projects/bar/src/app/foo.spec.ts');
104104
});
105105

106106
it('should create a standalone directive', async () => {
107107
const options = { ...defaultOptions, standalone: true };
108108
const tree = await schematicRunner.runSchematic('directive', options, appTree);
109-
const directiveContent = tree.readContent('/projects/bar/src/app/foo.directive.ts');
109+
const directiveContent = tree.readContent('/projects/bar/src/app/foo.ts');
110110
expect(directiveContent).not.toContain('standalone');
111-
expect(directiveContent).toContain('class FooDirective');
111+
expect(directiveContent).toContain('class Foo');
112112
});
113113

114114
it('should error when class name contains invalid characters', async () => {
@@ -119,6 +119,24 @@ describe('Directive Schematic', () => {
119119
).toBeRejectedWithError('Class name "404" is invalid.');
120120
});
121121

122+
it('should respect the type option', async () => {
123+
const options = { ...defaultOptions, type: 'Directive' };
124+
const tree = await schematicRunner.runSchematic('directive', options, appTree);
125+
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
126+
const testContent = tree.readContent('/projects/bar/src/app/foo.directive.spec.ts');
127+
expect(content).toContain('export class FooDirective');
128+
expect(testContent).toContain("describe('FooDirective'");
129+
});
130+
131+
it('should allow empty string in the type option', async () => {
132+
const options = { ...defaultOptions, type: '' };
133+
const tree = await schematicRunner.runSchematic('directive', options, appTree);
134+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
135+
const testContent = tree.readContent('/projects/bar/src/app/foo.spec.ts');
136+
expect(content).toContain('export class Foo');
137+
expect(testContent).toContain("describe('Foo'");
138+
});
139+
122140
describe('standalone=false', () => {
123141
const defaultNonStandaloneOptions: DirectiveOptions = {
124142
...defaultOptions,
@@ -139,11 +157,11 @@ describe('Directive Schematic', () => {
139157

140158
const tree = await schematicRunner.runSchematic('directive', options, appTree);
141159
const files = tree.files;
142-
expect(files).toContain('/projects/baz/src/app/foo.directive.spec.ts');
143-
expect(files).toContain('/projects/baz/src/app/foo.directive.ts');
160+
expect(files).toContain('/projects/baz/src/app/foo.spec.ts');
161+
expect(files).toContain('/projects/baz/src/app/foo.ts');
144162
const moduleContent = tree.readContent('/projects/baz/src/app/app.module.ts');
145-
expect(moduleContent).toMatch(/import.*Foo.*from'.\/foo.directive'/);
146-
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooDirective\r?\n/m);
163+
expect(moduleContent).toMatch(/import.*Foo.*from'.\/foo'/);
164+
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+Foo\r?\n/m);
147165
});
148166

149167
it('should respect the sourceRoot value', async () => {
@@ -167,7 +185,7 @@ describe('Directive Schematic', () => {
167185
appTree,
168186
);
169187

170-
expect(appTree.files).toContain('/projects/baz/custom/app/foo.directive.ts');
188+
expect(appTree.files).toContain('/projects/baz/custom/app/foo.ts');
171189
});
172190

173191
it('should find the closest module', async () => {
@@ -188,15 +206,15 @@ describe('Directive Schematic', () => {
188206

189207
const tree = await schematicRunner.runSchematic('directive', options, appTree);
190208
const fooModuleContent = tree.readContent(fooModule);
191-
expect(fooModuleContent).toMatch(/import{FooDirective}from'.\/foo.directive'/);
209+
expect(fooModuleContent).toMatch(/import{Foo}from'.\/foo'/);
192210
});
193211

194212
it('should export the directive', async () => {
195213
const options = { ...defaultNonStandaloneOptions, export: true };
196214

197215
const tree = await schematicRunner.runSchematic('directive', options, appTree);
198216
const appModuleContent = tree.readContent('/projects/baz/src/app/app.module.ts');
199-
expect(appModuleContent).toMatch(/exports:\[\n(\s*){2}FooDirective\n1円\]/);
217+
expect(appModuleContent).toMatch(/exports:\[\n(\s*){2}Foo\n1円\]/);
200218
});
201219

202220
it('should import into a specified module', async () => {
@@ -205,7 +223,7 @@ describe('Directive Schematic', () => {
205223
const tree = await schematicRunner.runSchematic('directive', options, appTree);
206224
const appModule = tree.readContent('/projects/baz/src/app/app.module.ts');
207225

208-
expect(appModule).toMatch(/import{FooDirective}from'.\/foo.directive'/);
226+
expect(appModule).toMatch(/import{Foo}from'.\/foo'/);
209227
});
210228

211229
it('should fail if specified module does not exist', async () => {

‎packages/schematics/angular/directive/schema.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
"type": "boolean",
8181
"default": false,
8282
"description": "Automatically export the directive from the specified NgModule, making it accessible to other modules in the application."
83+
},
84+
"type": {
85+
"type": "string",
86+
"description": "Append a custom type to the directive's filename. For example, if you set the type to `directive`, the file will be named `example.directive.ts`."
8387
}
8488
},
8589
"required": ["name", "project"]

‎tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export default function () {
66
const directiveDir = join('src', 'app');
77
return (
88
ng('generate', 'directive', 'test-directive')
9-
.then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.ts')))
10-
.then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.spec.ts')))
9+
.then(() => expectFileToExist(join(directiveDir, 'test-directive.ts')))
10+
.then(() => expectFileToExist(join(directiveDir, 'test-directive.spec.ts')))
1111

1212
// Try to run the unit tests.
1313
.then(() => ng('test', '--watch=false'))

‎tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ export default function () {
1616
}),
1717
)
1818
.then(() => ng('generate', 'directive', 'test2-directive'))
19-
.then(() =>
20-
expectFileToMatch(join(directiveDir, 'test2-directive.directive.ts'), /selector:'\[preW/),
21-
)
19+
.then(() => expectFileToMatch(join(directiveDir, 'test2-directive.ts'), /selector:'\[preW/))
2220
.then(() => ng('generate', 'application', 'app-two', '--skip-install'))
2321
.then(() => useCIDefaults('app-two'))
2422
.then(() => useCIChrome('app-two', './projects/app-two'))
@@ -33,17 +31,12 @@ export default function () {
3331
.then(() => ng('generate', 'directive', '--skip-import', 'test3-directive'))
3432
.then(() => process.chdir('../..'))
3533
.then(() =>
36-
expectFileToMatch(
37-
join('projects', 'app-two', 'test3-directive.directive.ts'),
38-
/selector:'\[preW/,
39-
),
34+
expectFileToMatch(join('projects', 'app-two', 'test3-directive.ts'), /selector:'\[preW/),
4035
)
4136
.then(() => process.chdir('src/app'))
4237
.then(() => ng('generate', 'directive', 'test-directive'))
4338
.then(() => process.chdir('../..'))
44-
.then(() =>
45-
expectFileToMatch(join(directiveDir, 'test-directive.directive.ts'), /selector:'\[preP/),
46-
)
39+
.then(() => expectFileToMatch(join(directiveDir, 'test-directive.ts'), /selector:'\[preP/))
4740

4841
// Try to run the unit tests.
4942
.then(() => ng('test', '--watch=false'))

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /