Skip to content

fix(forms): Support Signal Forms in form controls - #17566

Open
rkaraivanov wants to merge 7 commits into
masterfrom
rkaraivanov/fix-17556
Open

fix(forms): Support Signal Forms in form controls#17566
rkaraivanov wants to merge 7 commits into
masterfrom
rkaraivanov/fix-17556

Conversation

@rkaraivanov

Copy link
Copy Markdown
Member

Description

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

Type of Change (check all that apply):

  • Bug fix

How Has This Been Tested?

  • Unit tests

Checklist:

  • All relevant tags have been applied to this PR
  • This PR includes unit tests covering all the new code (test guidelines)
  • This PR includes CHANGELOG.MD updates for newly added functionality

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
@rkaraivanov
rkaraivanov requested review from ChronosSF and a lite review from Copilot September 2, 2026 13:52
@rkaraivanov rkaraivanov added ❌ status: awaiting-test PRs awaiting manual verification forms forms: validation Forms validation related, including ngModel.status aka VALID/INVALID/TOUCHED/PRISTINE etc. signal-forms labels Sep 2, 2026
@rkaraivanov rkaraivanov added the squash-merge Merge PR with "Squash and Merge" option label Sep 2, 2026

Copilot AI left a comment

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.

🔵 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 NgControlAdapter in core to provide a single, backend-aware access path for NgControl (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 NgControl internals 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 viktorkombov added 💥 status: in-test PRs currently being tested and removed ❌ status: awaiting-test PRs awaiting manual verification labels Sep 8, 2026

@viktorkombov viktorkombov left a comment

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.

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, because required detection 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 required marker 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.requiredTrue now 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 @if destruction and repeated mount/unmount, with no leaked emissions
  • switching [formField] between fields at runtime
  • submit() marking untouched fields touched
  • clear() 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

Comment thread projects/igniteui-angular/core/src/core/ng-control-adapter.ts
Comment thread projects/igniteui-angular/core/src/core/ng-control-adapter.ts
@viktorkombov

Copy link
Copy Markdown
Contributor

radio-group.directive.ts:524, inside initialize():

if (this.ngControl!.disabled) {
    button.disabled = this.ngControl!.disabled;
}

The write is guarded by the condition it's writing, so it only ever sets true:

isDisabled = signal(false);
f = form(this.model, p => disabled(p.season, { when: () => this.isDisabled() }));

Set isDisabled true, then false. IgxRadioComponent.disabled stays true on every button.

Not introduced here — IgxRadioGroupDirective has no setDisabledState, so with Reactive Forms control.disable() never reaches the buttons at all, and initialize() sits in an effect() that doesn't re-run for a non-signal ngControl.disabled. Signal Forms make that effect reactive, which is what surfaces the latch.

Also, going into disabled the native <input> stays clickable even though the component flag is true. The group writes the property without marking anything dirty, so an OnPush host never gets checked.

@viktorkombov

Copy link
Copy Markdown
Contributor
  • input.directive.ts:294 — master had if (this.ngControl.control) around this. It's unconditional now, and touchedChanges does this.ngControl.control!.events. Was the guard safe to drop, or can control still be null at ngAfterViewInit?
  • radio-group.directive.ts:516subscribe(() => this.invalid = false). The signal backend emits an initial value where the observable one emits nothing, so invalid gets wiped at a point master didn't wipe it.
  • A field in PENDING renders as INVALID on both backends. Pre-existing and unchanged here, but Signal Forms inherit it.

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.
@rkaraivanov

Copy link
Copy Markdown
Member Author

radio-group.directive.ts:524, inside initialize():

if (this.ngControl!.disabled) {
    button.disabled = this.ngControl!.disabled;
}

The write is guarded by the condition it's writing, so it only ever sets true:

isDisabled = signal(false);
f = form(this.model, p => disabled(p.season, { when: () => this.isDisabled() }));

Set isDisabled true, then false. IgxRadioComponent.disabled stays true on every button.

Not introduced here — IgxRadioGroupDirective has no setDisabledState, so with Reactive Forms control.disable() never reaches the buttons at all, and initialize() sits in an effect() that doesn't re-run for a non-signal ngControl.disabled. Signal Forms make that effect reactive, which is what surfaces the latch.

Also, going into disabled the native <input> stays clickable even though the component flag is true. The group writes the property without marking anything dirty, so an OnPush host never gets checked.

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.

@viktorkombov

Copy link
Copy Markdown
Contributor

radio-group.directive.ts:524, inside initialize():

....

Also, going into disabled the native <input> stays clickable even though the component flag is true. The group writes the property without marking anything dirty, so an OnPush host never gets checked.

Correction on this comment: the last paragraph ("Also, going into disabled the native <input> stays clickable...") was wrong. The native <input> does follow the component's disabled flag. My check was reading the wrong element, so it always came back false. On the code before your fix, disabling the group disabled the inputs as well. The part about the buttons never re-enabling was right. Nothing to change on your side, I just didn't want it left in the thread as a real bug.

@viktorkombov

Copy link
Copy Markdown
Contributor

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
#17618 [Radio Group] control.enable() enables options disabled in the template
#17619 [Input Group] Valid state shown for a field with no rules after switching [formField]
#17620 [Input Group] aria-required stays true after a conditional required rule turns off

They reproduce on rkaraivanov/fix-17556 branch. The first two might be worth a look before merging, since neither happens on master and both affect existing Reactive Forms setups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💥 status: in-test PRs currently being tested forms: validation Forms validation related, including ngModel.status aka VALID/INVALID/TOUCHED/PRISTINE etc. forms signal-forms squash-merge Merge PR with "Squash and Merge" option

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal Forms interop NgControl causes Ignite UI Angular form control initialization failure

5 participants