ionic-team / ionic-team/ionic-framework
bug: form status classes (ion-invalid etc.) not applied correctly with Angular option `eventCoalescing: true`
- Dominant language
- TypeScript
- Stars
- 52.7k
- Forks
- 13.3k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 51
Description
### Prerequisites
- [x] I have read the [Contributing Guidelines](https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#creating-an-issue).
- [x] I agree to follow the [Code of Conduct](https://ionicframework.com/code-of-conduct).
- [x] I have searched for [existing issues](https://github.com/ionic-team/ionic-framework/issues) that already report this problem, without success.
### Ionic Framework Version
v8.x
### Current Behavior
With the Angular option `eventCoalescing: true`, form validation styling behaves weirdly:
https://github.com/user-attachments/assets/37789873-49bc-436e-86f5-ce8f46b6cd8a
### Expected Behavior
In reference to the video above:
- After typing "1", the "valid" styling should be visible. Instead, it is only visible after typing "2"
- After clearing the input and moving focus out of it, it should be styled as "invalid", because it is touched and invalid. Instead, the styling only becomes "invalid" after triggering another blur event by clicking into and out of the form field.
- After typing "3", the input should be styled as "valid". Instead, it is still styled as "invalid", and only gets the "valid" styles after typing "4"
- After clearing the input field again, with focus still on the input, it should be styled as "invalid" because it is touched and invalid. Instead, the "invalid" styles only get applied after focus moves out of the input.
### Steps to Reproduce
1. Clone and run the reproduction repo below. The only special configuration in that repo is the following line in `app.config.ts`:
```ts
provideZoneChangeDetection({ eventCoalescing: true })
```
2. Click in and out of he input field to mark it as "touched". It will display as invalid because the field is marked as required.
3. Type a single character into the input field. It will still be visually shows as invalid, even though the FormControl state is valid.
4. Remove the character again. It will be shown as valid, even though the FormControl state is invalid.
### Code Reproduction URL
https://github.com/ReneZeidler/ionic-form-classes-bug
### Ionic Info
Ionic:
Ionic CLI : 7.2.0 (/home/r718037/.nvm/versions/node/v20.18.1/lib/node_modules/@ionic/cli)
Ionic Framework : @ionic/angular 8.4.3
@angular-devkit/build-angular : 19.2.1
@angular-devkit/schematics : 19.2.1
@angular/cli : 19.2.1
@ionic/angular-toolkit : 12.1.1
Utility:
cordova-res : not installed globally
native-run : not installed globally
System:
NodeJS : v20.18.1 (/home/r718037/.nvm/versions/node/v20.18.1/bin/node)
npm : 11.0.0
OS : Linux 5.15
### Additional Information
`eventCoalescing` is an [Angular option](https://angular.dev/api/core/NgZoneOptions#eventCoalescing) to avoid running change detection multiple times for a single event. (The option `runCoalescing` coalesces change detection runs in even more cases, and implies the `eventCoalescing` option.)
What this means in practice is that change detection no longer gets run synchronously but asynchronously with `requestAnimationFrame` or `setTimeout`, whichever is faster. See the relevant code here:
https://github.com/angular/angular/blob/cae1fe519b4fc093ca99d0183a2c4da86a96bde1/packages/core/src/zone/ng_zone.ts#L381-L418
https://github.com/angular/angular/blob/cae1fe519b4fc093ca99d0183a2c4da86a96bde1/packages/core/src/util/callback_scheduler.ts#L11-L66
(The Angular documentation mentions this for `runCoalescing`, but not for `eventCoalescing`, even though it applies in both cases.)
---
The change detection running asynchronously seems to not play nicely with the logic that sets the Ionic form status classes (`ion-invalid`, `ion-touched`, etc.).
The method [`setIonicClasses`](https://github.com/ionic-team/ionic-framework/blob/4df0e0f4c00faec33f5ddc802945bf4ad9dc53d3/packages/angular/common/src/directives/control-value-accessors/value-accessor.ts#L113-L128) gets called synchronously whenever the classes could change, e.g. in `ionInput` event handlers or by hooking into Angular's FormControl's `markAs*` methods. The method then delays the actual updating of the classes with `requestAnimationFrame` before looking at the corresponding `ng-`classes to mirror. This usually gives change detection time to run, in which the `ng-`classes are simply [set via property binding](https://github.com/angular/angular/blob/cae1fe519b4fc093ca99d0183a2c4da86a96bde1/packages/forms/src/directives/ng_control_status.ts#L74-L82).
However, with the `eventCoalescing` option enabled, the change detection now also gets delayed by `requestAnimationFrame`. Because `setIonicClasses` called `raf` first, it also gets executed first, and at that point the `ng-`classes haven't been updated yet.
(Technically, because change detection delays itself by a race between `raf` and `setTimeout`, it sometimes still wins. On my system this is rare, but that causes this bug to be slightly inconsistent. How often `setTimeout` wins depends on the overall system power and screen refresh rate.)
`setIonicClasses` needs to be guranteed to run after a full change detection cycle for the logic to be correct.
Contributor guide
Assessment
This issue has not been assessed yet.