fix(forms): Support Signal Forms in form controls - #17566
Conversation
The `[formField]` interop `NgControl` exposes signal-backed getters only. It has no `statusChanges`, `valueChanges`, `validator`, `markAsTouched` or `setValue`, so igxInput, checkbox, switch, radio group, select, combo, simple combo and the date, time and date range pickers threw on init. Add `NgControlAdapter` in core as the single access path to the bound `NgControl`. It detects the backend and derives the missing observables from a root effect over the signal getters, keeping change detection order identical to the observable case. Controls no longer read `NgControl` internals directly. Closes #17556
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new core abstraction (NgControlAdapter) and updates multiple form controls’ initialization/validation wiring, which warrants final human review despite the added tests.
Pull request overview
This PR fixes initialization/runtime failures when Ignite UI form controls are bound via Angular Signal Forms ([formField]) by routing all NgControl access through a new adapter that normalizes missing observable APIs and control methods in the signal-backed interop control.
Changes:
- Added
NgControlAdapterincoreto provide a single, backend-aware access path forNgControl(observable vs signal). - Updated multiple form controls (input, checkbox/switch base, radio group, select, combo/simple-combo, date/time pickers) to use the adapter instead of reading
NgControlinternals directly. - Added Signal Forms-focused unit tests for the affected components and updated docs/README/CHANGELOG to document the new compatibility.
File summaries
| File | Description |
|---|---|
| skills/igniteui-angular-components/references/form-controls.md | Documents how to use Ignite UI controls with Signal Forms via [formField]. |
| projects/igniteui-angular/time-picker/src/time-picker/time-picker.component.ts | Uses NgControlAdapter for required/validity/status handling under Signal Forms. |
| projects/igniteui-angular/time-picker/src/time-picker/time-picker.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/switch/src/switch/switch.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/simple-combo/src/simple-combo/simple-combo.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/select/src/select/select.component.ts | Switches status/required/validity logic to the adapter for Signal Forms support. |
| projects/igniteui-angular/select/src/select/select.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.ts | Uses the adapter to safely consume status/required/validators under Signal Forms. |
| projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.spec.ts | Adds Signal Forms coverage for required/invalid behaviors. |
| projects/igniteui-angular/input-group/src/input-group/directives-input/input.directive.ts | Uses the adapter for status/value/touched tracking and write/touch interop. |
| projects/igniteui-angular/input-group/src/input-group/directives-input/input.directive.spec.ts | Adds Signal Forms coverage for required/invalid/disabled/reset behaviors. |
| projects/igniteui-angular/input-group/README.md | Notes igxInput compatibility with Signal Forms ([formField]). |
| projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts | Switches checkbox/switch validity & required resolution to the adapter. |
| projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.ts | Uses the adapter for status/required/validity + signal-backend revalidation hook. |
| projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker-inputs.common.ts | Uses adapter-based setValue handling to support signal-backend “ignored write” semantics. |
| projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.ts | Switches status/required/validity logic to the adapter for Signal Forms support. |
| projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/core/src/public_api.ts | Exports the new NgControlAdapter from the core public API. |
| projects/igniteui-angular/core/src/core/ng-control-adapter.ts | Introduces NgControlAdapter and signal-backed observable derivations via root effects. |
| projects/igniteui-angular/combo/src/combo/combo.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| projects/igniteui-angular/combo/src/combo/combo.common.ts | Switches combo validity/required/status wiring to use the adapter. |
| projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.spec.ts | Adds Signal Forms coverage for required/invalid/disabled behaviors. |
| CHANGELOG.md | Adds an Unreleased entry documenting Signal Forms compatibility across form controls. |
Review details
- Files reviewed: 24/24 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
viktorkombov
left a comment
There was a problem hiding this comment.
Tested on a zoneless demo page (provideZonelessChangeDetection()) with [formField] on each of the ten controls. All six also reproduce under zone.js, so none of them are zoneless-specific.
Two blocking:
- Checkbox / switch / radio throw on any validator that reads
control.value, becauserequireddetection now probes with{}. Reactive Forms path, no Signal Forms involved. Confirmed as a regression by reverting the two files to master individually. - Radio group gets stuck disabled under Signal Forms — it disables but never re-enables. Looks pre-existing rather than introduced here, but this is what makes it reachable.
Four more, not blocking:
- The conditional
requiredmarker goes stale on select, combo and both pickers. It corrects on the user's first interaction with the field, then sticks for good. - A satisfied custom rule never reaches
VALID, where Reactive Forms do. - The date range picker ends up drawing the asterisk for a rule that is switched off. Half of that one is pre-existing and hits Reactive Forms too.
Validators.requiredTruenow counts as required on checkbox/switch/radio where master said no.
Also checked, and clean, so nobody re-treads them:
- the adapter's effects tear down on
@ifdestruction and repeated mount/unmount, with no leaked emissions - switching
[formField]between fields at runtime submit()marking untouched fields touchedclear()writing back to the model- the date range picker's projected two-input write fallback, on both backends
- rapid model updates coalescing into one transition
|
if (this.ngControl!.disabled) {
button.disabled = this.ngControl!.disabled;
}The write is guarded by the condition it's writing, so it only ever sets isDisabled = signal(false);
f = form(this.model, p => disabled(p.season, { when: () => this.isDisabled() }));Set Not introduced here — Also, going into disabled the native |
|
The `required` probe threw for validators that read the control value, a regression for checkbox and radio. `IgxRadioGroupDirective` had no `setDisabledState`, so neither Signal Forms nor `control.disable()` could re-enable the buttons. On the signal backend `statusChanges` did not watch `required`, and a custom rule that was never violated never reached the valid state. The date range picker writes the required marker in a microtask; mark for check so a required-only change repaints without a status flip.
Fixed. IgxRadioGroupDirective implements setDisabledState: stores the state, writes every button, marks for check. initialize() applies the stored state to late buttons and no longer reads ngControl.disabled. Covers control.disable() / enable() and the Signal Forms rule. Specs for both. |
....
Correction on this comment: the last paragraph ("Also, going into disabled the native |
|
I've logged the remaining findings as separate issues so they can be handled outside this PR: #17617 [Checkbox] required is lost when combined with a validator that reads the value They reproduce on |
Description
The
[formField]interopNgControlexposes signal-backed getters only. It has nostatusChanges,valueChanges,validator,markAsTouchedorsetValue, so igxInput, checkbox, switch, radio group, select, combo, simple combo and the date, time and date range pickers threw on init.Add
NgControlAdapterin core as the single access path to the boundNgControl. It detects the backend and derives the missing observables from a root effect over the signal getters, keeping change detection order identical to the observable case. Controls no longer readNgControlinternals directly.Closes #17556
Type of Change (check all that apply):
How Has This Been Tested?
Checklist:
CHANGELOG.MDupdates for newly added functionality