diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
index 3ddcc9cd2fe..9aec91b7214 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
@@ -2574,4 +2574,123 @@ describe('type check blocks', () => {
expect(block).toContain('_t1.input = (((this).value));');
});
});
+
+ describe('signal forms', () => {
+ let FieldMock: TestDirective;
+
+ beforeEach(() => {
+ setup([]);
+
+ FieldMock = {
+ type: 'directive',
+ name: 'Field',
+ selector: '[field]',
+ bestGuessOwningModule: {
+ specifier: '@angular/forms/signals',
+ resolutionContext: '',
+ },
+ inputs: {
+ field: 'field',
+ },
+ };
+ });
+
+ it('should generate a string field for an input without a type', () => {
+ const block = tcb('', [FieldMock]);
+ expect(block).toContain('var _t1 = null! as i0.Field;');
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+
+ [
+ {inputType: 'text', expectedType: 'string'},
+ {inputType: 'radio', expectedType: 'string'},
+ {inputType: 'checkbox', expectedType: 'boolean'},
+ {inputType: 'number', expectedType: 'string | number'},
+ {inputType: 'range', expectedType: 'string | number'},
+ {inputType: 'datetime-local', expectedType: 'string | number'},
+ {inputType: 'date', expectedType: 'string | number | Date | null'},
+ {inputType: 'month', expectedType: 'string | number | Date | null'},
+ {inputType: 'time', expectedType: 'string | number | Date | null'},
+ {inputType: 'week', expectedType: 'string | number | Date | null'},
+ {inputType: 'unknown', expectedType: 'string'},
+ ].forEach(({inputType, expectedType}) => {
+ it(`should generate a '${expectedType}' field for an input with a '${inputType}' type`, () => {
+ const block = tcb(``, [FieldMock]);
+ expect(block).toContain(`var _t1 = null! as i0.Field<${expectedType}>;`);
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+ });
+
+ it('should generate a string field for a textarea', () => {
+ const block = tcb('', [FieldMock]);
+ expect(block).toContain('var _t1 = null! as i0.Field;');
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+
+ it('should generate a string field for a select', () => {
+ const block = tcb('', [FieldMock]);
+ expect(block).toContain('var _t1 = null! as i0.Field;');
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+
+ it('should generate a custom value control', () => {
+ const block = tcb('', [
+ FieldMock,
+ {
+ type: 'directive',
+ name: 'CustomControl',
+ selector: 'custom-control',
+ inputs: {
+ value: {
+ classPropertyName: 'value',
+ bindingPropertyName: 'value',
+ required: false,
+ isSignal: true,
+ transform: null,
+ },
+ },
+ outputs: {
+ valueChange: 'valueChange',
+ },
+ },
+ ]);
+
+ expect(block).toContain(
+ 'var _t1 = null! as i0.Field>;',
+ );
+ expect(block).toContain('var _t2 = null! as i2.FormValueControl;');
+ expect(block).toContain('if (_t2) _t2 = null! as i0.CustomControl;');
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+
+ it('should generate a custom checkbox control', () => {
+ const block = tcb('', [
+ FieldMock,
+ {
+ type: 'directive',
+ name: 'CustomControl',
+ selector: 'custom-control',
+ inputs: {
+ checked: {
+ classPropertyName: 'checked',
+ bindingPropertyName: 'checked',
+ required: false,
+ isSignal: true,
+ transform: null,
+ },
+ },
+ outputs: {
+ checkedChange: 'checkedChange',
+ },
+ },
+ ]);
+
+ expect(block).toContain(
+ 'var _t1 = null! as i0.Field>;',
+ );
+ expect(block).toContain('var _t2 = null! as i2.FormCheckboxControl;');
+ expect(block).toContain('if (_t2) _t2 = null! as i0.CustomControl;');
+ expect(block).toContain('_t1.field = (((this).f));');
+ });
+ });
});
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
index 86994e7702d..c784ed1a847 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
@@ -47,6 +47,7 @@ import {
LocalIdentifierStrategy,
LogicalProjectStrategy,
ModuleResolver,
+ OwningModule,
Reference,
ReferenceEmitter,
RelativePathStrategy,
@@ -68,6 +69,7 @@ import {
import {NOOP_PERF_RECORDER} from '../../perf';
import {TsCreateProgramDriver} from '../../program_driver';
import {
+ AmbientImport,
ClassDeclaration,
isNamedClassDeclaration,
TypeScriptReflectionHost,
@@ -341,6 +343,7 @@ export interface TestDirective
inputs?: string[];
outputs?: string[];
}[];
+ bestGuessOwningModule?: OwningModule | AmbientImport;
}
export interface TestPipe {
@@ -350,6 +353,7 @@ export interface TestPipe {
pipeName: string;
type: 'pipe';
code?: string;
+ bestGuessOwningModule?: OwningModule | AmbientImport;
}
export type TestDeclaration = TestDirective | TestPipe;
@@ -817,7 +821,7 @@ function prepareDeclarations(
} else if (decl.type === 'pipe') {
pipes.set(decl.pipeName, {
kind: MetaKind.Pipe,
- ref: new Reference(resolveDeclaration(decl)),
+ ref: new Reference(resolveDeclaration(decl), decl.bestGuessOwningModule),
name: decl.pipeName,
nameExpr: null,
isStandalone: false,
@@ -864,7 +868,7 @@ function getDirectiveMetaFromDeclaration(
) {
return {
name: decl.name,
- ref: new Reference(resolveDeclaration(decl)),
+ ref: new Reference(resolveDeclaration(decl), decl.bestGuessOwningModule),
exportAs: decl.exportAs || null,
selector: decl.selector || null,
hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard || false,
diff --git a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
index 0bc8a083859..c182487989c 100644
--- a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
@@ -104,93 +104,31 @@ runInEachFileSystem(() => {
);
});
- [
- {inputType: 'text', expectedType: 'string'},
- {inputType: 'radio', expectedType: 'string'},
- {inputType: 'checkbox', expectedType: 'boolean'},
- {inputType: 'number', expectedType: 'string | number'},
- {inputType: 'range', expectedType: 'string | number'},
- {inputType: 'datetime-local', expectedType: 'string | number'},
- {inputType: 'date', expectedType: 'string | number | Date | null'},
- {inputType: 'month', expectedType: 'string | number | Date | null'},
- {inputType: 'time', expectedType: 'string | number | Date | null'},
- {inputType: 'week', expectedType: 'string | number | Date | null'},
- {inputType: 'unknown', expectedType: 'string'},
- ].forEach(({inputType, expectedType}) => {
- it(`should infer an input with '${inputType}' type as a '${expectedType}' field`, () => {
- env.write(
- 'test.ts',
- `
+ it('should infer the type of the field from the input `type`', () => {
+ env.write(
+ 'test.ts',
+ `
import {Component, signal} from '@angular/core';
import {Field, form} from '@angular/forms/signals';
@Component({
- template: '',
+ template: '',
imports: [Field]
})
export class Comp {
f = form(signal(null as unknown));
}
`,
- );
-
- const diags = env.driveDiagnostics();
- expect(diags.length).toBe(1);
- expect(extractMessage(diags[0])).toBe(
- `Type '() => FieldState' is not assignable to type '() => FieldState<${expectedType}, string | number>'.`,
- );
- });
- });
-
- it('should infer a `textarea` as a string field', () => {
- env.write(
- 'test.ts',
- `
- import {Component, signal} from '@angular/core';
- import {Field, form} from '@angular/forms/signals';
-
- @Component({
- template: '',
- imports: [Field]
- })
- export class Comp {
- f = form(signal(0));
- }
- `,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(extractMessage(diags[0])).toBe(
- `Type '() => FieldState' is not assignable to type '() => FieldState'.`,
+ `Type '() => FieldState' is not assignable to type '() => FieldState'.`,
);
});
- it('should infer a `select` as a string field', () => {
- env.write(
- 'test.ts',
- `
- import {Component, signal} from '@angular/core';
- import {Field, form} from '@angular/forms/signals';
-
- @Component({
- template: '',
- imports: [Field]
- })
- export class Comp {
- f = form(signal(0));
- }
- `,
- );
-
- const diags = env.driveDiagnostics();
- expect(diags.length).toBe(1);
- expect(extractMessage(diags[0])).toBe(
- `Type '() => FieldState' is not assignable to type '() => FieldState'.`,
- );
- });
-
- it('should infer the type of a custom form field control', () => {
+ it('should infer the type of a custom value control', () => {
env.write(
'test.ts',
`