diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts index 3a18a279c82..1f1723467fc 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts @@ -46,6 +46,7 @@ const formControlInputFields = [ 'hidden', 'invalid', 'name', + 'pending', 'readonly', 'touched', 'max', diff --git a/packages/forms/signals/test/web/field_directive.spec.ts b/packages/forms/signals/test/web/field_directive.spec.ts index fd5be657f77..968d41a2db1 100644 --- a/packages/forms/signals/test/web/field_directive.spec.ts +++ b/packages/forms/signals/test/web/field_directive.spec.ts @@ -545,6 +545,97 @@ describe('field directive', () => { }); }); + describe('pending', () => { + it('should bind to custom control', async () => { + const {promise, resolve} = promiseWithResolvers(); + + @Component({ + selector: 'custom-control', + template: '', + }) + class CustomControl implements FormValueControl { + readonly value = model.required(); + readonly pending = input.required(); + } + + @Component({ + template: ` `, + imports: [CustomControl, Field], + }) + class TestCmp { + readonly data = signal('test'); + readonly f = form(this.data, (p) => { + validateAsync(p, { + params: () => [], + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: (results) => results, + onError: () => null, + }); + }); + readonly customControl = viewChild.required(CustomControl); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const comp = fixture.componentInstance; + + expect(comp.customControl().pending()).toBe(true); + + resolve([]); + await promise; + await fixture.whenStable(); + + expect(comp.customControl().pending()).toBe(false); + }); + + it('should be reset when field changes on custom control', async () => { + const {promise, resolve} = promiseWithResolvers(); + + @Component({selector: 'custom-control', template: ``}) + class CustomControl implements FormValueControl { + readonly value = model.required(); + readonly pending = input.required(); + } + + @Component({ + imports: [Field, CustomControl], + template: ``, + }) + class TestCmp { + readonly f = form(signal({x: '', y: ''}), (p) => { + validateAsync(p.x, { + params: () => [], + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: (results) => results, + onError: () => null, + }); + }); + readonly field = signal(this.f.x); + readonly customControl = viewChild.required(CustomControl); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + const component = fixture.componentInstance; + + expect(component.customControl().pending()).toBe(true); + + act(() => component.field.set(component.f.y)); + expect(component.customControl().pending()).toBe(false); + + resolve([]); + await promise; + await fixture.whenStable(); + expect(component.customControl().pending()).toBe(false); + }); + }); + describe('readonly', () => { it('should bind to native control', () => { @Component({ @@ -2184,49 +2275,6 @@ describe('field directive', () => { }); }); - it('should synchronize pending status', async () => { - const {promise, resolve} = promiseWithResolvers(); - - @Component({ - selector: 'my-input', - template: '', - }) - class CustomInput implements FormValueControl { - value = model(''); - pending = input(false); - } - - @Component({ - template: ` `, - imports: [CustomInput, Field], - }) - class PendingTestCmp { - myInput = viewChild.required(CustomInput); - data = signal('test'); - f = form(this.data, (p) => { - validateAsync(p, { - params: () => [], - factory: (params) => - resource({ - params, - loader: () => promise, - }), - onSuccess: (results) => results, - onError: () => null, - }); - }); - } - - const fix = act(() => TestBed.createComponent(PendingTestCmp)); - - expect(fix.componentInstance.myInput().pending()).toBe(true); - - resolve([]); - await promise; - await fix.whenStable(); - expect(fix.componentInstance.myInput().pending()).toBe(false); - }); - it(`should mark field as touched on native control 'blur' event`, () => { @Component({ imports: [Field],