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', `