Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/forms/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,8 +1022,11 @@ export class FormControl extends AbstractControl {
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
this._applyFormState(formState);
this._setUpdateStrategy(validatorOrOpts);
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._initObservables();
this.updateValueAndValidity({
onlySelf: true,
emitEvent: asyncValidator ? true : false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what others think, but I would be tempted to make this shorter by using !!asyncValidator.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer shorter version as well 👍

}); // `asyncValidator` will cause status change, and that needs to be notified

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose moving the comment above the corresponding line + use !!asyncValidator as Pete proposed, something like this (I've also updated a comment a bit):

this.updateValueAndValidity({
  onlySelf: true,
  // If `asyncValidator` is present, it will trigger control status change from `PENDING` to `VALID` or `INVALID`.
  // The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent` to `true`
  // to allow that during the control creation process.
  emitEvent: !!asyncValidator
});

The same/similar comment can be added to other places as well (FormGroup and FormArray).

}

/**
Expand Down Expand Up @@ -1272,7 +1275,10 @@ export class FormGroup extends AbstractControl {
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this.updateValueAndValidity({
onlySelf: true,
emitEvent: asyncValidator ? true : false
}); //`asyncValidator` will cause status change, and that needs to be notified
}

/**
Expand Down Expand Up @@ -1689,7 +1695,10 @@ export class FormArray extends AbstractControl {
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this.updateValueAndValidity({
onlySelf: true,
emitEvent: asyncValidator ? true : false
}); // `asyncValidator` will cause status change, and that needs to be notified
}

/**
Expand Down
30 changes: 30 additions & 0 deletions packages/forms/test/form_group_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,36 @@ import {of } from 'rxjs';
expect(logger).toEqual(['change!']);
});

it('should only emit (`VALID`) status change event in absence of `FormControl.asyncValidator`',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding tests 👍

I think we should increase test coverage a bit more to handle additional cases, for example:

  • Standalone FormControl with async validator and both statusChanges and valueChanges subscriptions and log what the output is. We should have 2 tests here tp cover both VALID and INVALID results of the validation.

  • Standalone FormControl with both sync and async validators and see what happens in statusChanges and valueChanges subscriptions when sync validators return true/false and async validator returns true/false.

  • Combination of FormGroup and a child FormControl to test the behavior when we initialize such combination and:

    • async validator is present on a FormControl level
    • async validator is present on both FormGroup and a child FormControl level
  • Similar to the previous item, but cover the setControl case (this is what is partially implemented already).

  • Similar to the one above, the combination of FormArray and a child FormControl: initialization phase and a case with calling some API methods that might trigger status change (like pushing new controls to the array).

  • Combination of all classes: FormGroup, FormArray (as a child to the FormGroup) and FormControl (as a child to FormArray).

Once we have improved coverage, we can also see what the difference is in what we emit into the statusChanges and valueChanges now vs what the new set of events are. That should help better formalize the "breaking changes" note to let developers know which changes to expect and how to handle them (if needed).

Please let me know if any additional information is required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick comment to the previous one: I think it'd also be great to have a test that would replace/remove a control (via setControl and/or other methods) which is running an async validator at this moment (i.e. in PENDING state), to see which events would be emitted in statusChanges.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, one more comment: it looks like PR #22575 might also be related to your PR, since it fixes the problem with async validation status, so you may see some incorrect results (described in #22575) in the statusChanges log until #22575 is accepted/merged.

fakeAsync(() => {
const logger: string[] = [];
const c2 = new FormControl('new!');
g.statusChanges.subscribe((status) => logger.push(status));
g.setControl('one', c2);
tick();
expect(logger).toEqual(['VALID']);
}));

it('should emit (`PENDING, VALID`) status change events, if `FormControl.asyncValidator` leads to success',
fakeAsync(() => {
const logger: string[] = [];
const c2 = new FormControl('new!', null, asyncValidator('new!'));
g.statusChanges.subscribe((status) => logger.push(status));
g.setControl('one', c2);
tick();
expect(logger).toEqual(['PENDING', 'VALID']);
}));

it('should emit (`PENDING, INVALID`) status change eventse, if `FormControl.asyncValidator` leads to errors',
fakeAsync(() => {
const logger: string[] = [];
const c2 = new FormControl('new!', null, asyncValidator('expected'));
g.statusChanges.subscribe((status) => logger.push(status));
g.setControl('one', c2);
tick();
expect(logger).toEqual(['PENDING', 'INVALID']);
}));

});

describe('pending', () => {
Expand Down