Skip to content

Commit ec110f1

Browse files
leonsenftcrisbeto
authored andcommitted
fix(forms): allow custom controls to require pending input
* Allow custom controls to make `pending` a required input * Refactor test for `pending` input to be consistent with other control properties * Test that `pending` inputs are reset when the field binding changes (cherry picked from commit 1a4c3eb)
1 parent 79d1691 commit ec110f1

File tree

2 files changed

+92
-43
lines changed

2 files changed

+92
-43
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const formControlInputFields = [
4646
'hidden',
4747
'invalid',
4848
'name',
49+
'pending',
4950
'readonly',
5051
'touched',
5152
'max',

packages/forms/signals/test/web/field_directive.spec.ts

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,97 @@ describe('field directive', () => {
545545
});
546546
});
547547

548+
describe('pending', () => {
549+
it('should bind to custom control', async () => {
550+
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
551+
552+
@Component({
553+
selector: 'custom-control',
554+
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
555+
})
556+
class CustomControl implements FormValueControl<string> {
557+
readonly value = model.required<string>();
558+
readonly pending = input.required<boolean>();
559+
}
560+
561+
@Component({
562+
template: ` <custom-control [field]="f" /> `,
563+
imports: [CustomControl, Field],
564+
})
565+
class TestCmp {
566+
readonly data = signal('test');
567+
readonly f = form(this.data, (p) => {
568+
validateAsync(p, {
569+
params: () => [],
570+
factory: (params) =>
571+
resource({
572+
params,
573+
loader: () => promise,
574+
}),
575+
onSuccess: (results) => results,
576+
onError: () => null,
577+
});
578+
});
579+
readonly customControl = viewChild.required(CustomControl);
580+
}
581+
582+
const fixture = act(() => TestBed.createComponent(TestCmp));
583+
const comp = fixture.componentInstance;
584+
585+
expect(comp.customControl().pending()).toBe(true);
586+
587+
resolve([]);
588+
await promise;
589+
await fixture.whenStable();
590+
591+
expect(comp.customControl().pending()).toBe(false);
592+
});
593+
594+
it('should be reset when field changes on custom control', async () => {
595+
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
596+
597+
@Component({selector: 'custom-control', template: ``})
598+
class CustomControl implements FormValueControl<string> {
599+
readonly value = model.required<string>();
600+
readonly pending = input.required<boolean>();
601+
}
602+
603+
@Component({
604+
imports: [Field, CustomControl],
605+
template: `<custom-control [field]="field()" />`,
606+
})
607+
class TestCmp {
608+
readonly f = form(signal({x: '', y: ''}), (p) => {
609+
validateAsync(p.x, {
610+
params: () => [],
611+
factory: (params) =>
612+
resource({
613+
params,
614+
loader: () => promise,
615+
}),
616+
onSuccess: (results) => results,
617+
onError: () => null,
618+
});
619+
});
620+
readonly field = signal(this.f.x);
621+
readonly customControl = viewChild.required(CustomControl);
622+
}
623+
624+
const fixture = act(() => TestBed.createComponent(TestCmp));
625+
const component = fixture.componentInstance;
626+
627+
expect(component.customControl().pending()).toBe(true);
628+
629+
act(() => component.field.set(component.f.y));
630+
expect(component.customControl().pending()).toBe(false);
631+
632+
resolve([]);
633+
await promise;
634+
await fixture.whenStable();
635+
expect(component.customControl().pending()).toBe(false);
636+
});
637+
});
638+
548639
describe('readonly', () => {
549640
it('should bind to native control', () => {
550641
@Component({
@@ -2184,49 +2275,6 @@ describe('field directive', () => {
21842275
});
21852276
});
21862277

2187-
it('should synchronize pending status', async () => {
2188-
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
2189-
2190-
@Component({
2191-
selector: 'my-input',
2192-
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
2193-
})
2194-
class CustomInput implements FormValueControl<string> {
2195-
value = model('');
2196-
pending = input(false);
2197-
}
2198-
2199-
@Component({
2200-
template: ` <my-input [field]="f" /> `,
2201-
imports: [CustomInput, Field],
2202-
})
2203-
class PendingTestCmp {
2204-
myInput = viewChild.required<CustomInput>(CustomInput);
2205-
data = signal('test');
2206-
f = form(this.data, (p) => {
2207-
validateAsync(p, {
2208-
params: () => [],
2209-
factory: (params) =>
2210-
resource({
2211-
params,
2212-
loader: () => promise,
2213-
}),
2214-
onSuccess: (results) => results,
2215-
onError: () => null,
2216-
});
2217-
});
2218-
}
2219-
2220-
const fix = act(() => TestBed.createComponent(PendingTestCmp));
2221-
2222-
expect(fix.componentInstance.myInput().pending()).toBe(true);
2223-
2224-
resolve([]);
2225-
await promise;
2226-
await fix.whenStable();
2227-
expect(fix.componentInstance.myInput().pending()).toBe(false);
2228-
});
2229-
22302278
it(`should mark field as touched on native control 'blur' event`, () => {
22312279
@Component({
22322280
imports: [Field],

0 commit comments

Comments
 (0)