Skip to content

Commit e8eb5b9

Browse files
committed
fix(forms/signals): propagate pending state through grandchild fields
The `pending()` signal only checked direct children's `asyncErrors` for pending sentinels, so a grandchild's async validator would not bubble the pending state up to the grandparent. This caused `InteropNgControl.status` to throw an AssertionError when the field tree had three or more levels and an async validator was placed on a grandchild field (status was 'unknown' but `pending()` returned false). - Fix `FieldValidationState.pending` to recursively call `child.validationState.pending()` instead of checking `child.validationState.asyncErrors().includes('pending')`. - Replace the unreachable `throw` in `InteropNgControl.status` with a defensive `return 'PENDING'` fallback that correctly maps the 'unknown' validation status to Angular's FormControlStatus. - Add a regression test for the grandchild pending propagation scenario. https://claude.ai/code/session_01VdGf6HYafkbRfpEBJYL9Te
1 parent 59e6489 commit e8eb5b9

File tree

3 files changed

+38
-7
lines changed

3 files changed

+38
-7
lines changed

packages/forms/signals/src/controls/interop_ng_control.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,13 @@ export class InteropNgControl
114114
if (this.field().disabled()) {
115115
return 'DISABLED';
116116
}
117-
if (this.field().valid()) {
118-
return 'VALID';
119-
}
120117
if (this.field().invalid()) {
121118
return 'INVALID';
122119
}
123-
if (this.field().pending()) {
124-
return 'PENDING';
120+
if (this.field().valid()) {
121+
return 'VALID';
125122
}
126-
throw Error('AssertionError: unknown form control status');
123+
return 'PENDING';
127124
}
128125

129126
valueAccessor: ControlValueAccessor | null = null;

packages/forms/signals/src/field/validation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export class FieldValidationState implements ValidationState {
262262
reduceChildren(
263263
this.node,
264264
this.asyncErrors().includes('pending'),
265-
(child, value) => value || child.validationState.asyncErrors().includes('pending'),
265+
(child, value) => value || child.validationState.pending(),
266266
),
267267
);
268268

packages/forms/signals/test/node/validation_status.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,40 @@ describe('validation status', () => {
416416
expect(f().valid()).toBe(false);
417417
expect(f().invalid()).toBe(true);
418418
});
419+
420+
it('grandparent should be pending while grandchild async validator is pending', async () => {
421+
let res: Resource<unknown>;
422+
423+
const f = form(
424+
signal({child: {grandchild: 'VALID'}}),
425+
(p) => {
426+
validateAsync(p.child.grandchild, {
427+
params: ({value}) => value(),
428+
factory: (params) =>
429+
(res = resource({
430+
params,
431+
loader: ({params}) =>
432+
new Promise<ValidationError[]>((r) =>
433+
setTimeout(() => r(validateValueForChild(params, undefined))),
434+
),
435+
})),
436+
onSuccess: (results) => results,
437+
onError: () => null,
438+
});
439+
},
440+
{injector},
441+
);
442+
443+
expect(f().pending()).toBe(true);
444+
expect(f().valid()).toBe(false);
445+
expect(f().invalid()).toBe(false);
446+
447+
await appRef.whenStable();
448+
449+
expect(f().pending()).toBe(false);
450+
expect(f().valid()).toBe(true);
451+
expect(f().invalid()).toBe(false);
452+
});
419453
});
420454

421455
describe('multiple validators', () => {

0 commit comments

Comments
 (0)