Skip to content

Commit de56d74

Browse files
alxhubatscott
authored andcommitted
fix(forms): align FormField CVA selection priority with standard forms
Prioritize custom ControlValueAccessor instances over default or built-in accessors when applying the [formField] directive. This is achieved by directly consuming selectValueAccessor from @angular/forms, ensuring absolute alignment with the precedence rules used across standard Angular form directives.
1 parent 2e9aeea commit de56d74

File tree

4 files changed

+45
-4
lines changed

4 files changed

+45
-4
lines changed

packages/forms/signals/src/directive/form_field.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
NG_VALUE_ACCESSOR,
3131
NgControl,
3232
ɵFORM_FIELD_PARSE_ERRORS as FORM_FIELD_PARSE_ERRORS,
33+
ɵselectValueAccessor as selectValueAccessor,
3334
} from '@angular/forms';
3435
import {type ValidationError} from '../api/rules';
3536
import type {Field, FieldState} from '../api/types';
@@ -193,7 +194,18 @@ export class FormField<T> {
193194
* @internal
194195
*/
195196
get controlValueAccessor(): ControlValueAccessor | undefined {
196-
return this.controlValueAccessors?.[0] ?? this.interopNgControl?.valueAccessor ?? undefined;
197+
if (!this.controlValueAccessors || this.controlValueAccessors.length === 0) {
198+
return this.interopNgControl?.valueAccessor ?? undefined;
199+
}
200+
201+
// Rely on the exact logic in `@angular/forms` to pick the accessors with correct priority,
202+
// passing our fake `InteropNgControl` to fulfill its first parameter requirement.
203+
return (
204+
selectValueAccessor(
205+
this.interopNgControl as unknown as NgControl,
206+
this.controlValueAccessors,
207+
) ?? undefined
208+
);
197209
}
198210

199211
/**

packages/forms/signals/test/web/interop.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
viewChild,
1919
} from '@angular/core';
2020
import {TestBed} from '@angular/core/testing';
21-
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl} from '@angular/forms';
21+
import {
22+
ControlValueAccessor,
23+
DefaultValueAccessor,
24+
NG_VALUE_ACCESSOR,
25+
NgControl,
26+
ReactiveFormsModule,
27+
} from '@angular/forms';
2228
import {
2329
debounce,
2430
disabled,
@@ -373,6 +379,25 @@ describe('ControlValueAccessor', () => {
373379
expect(() => fixture.componentInstance.disabled.set(true)).not.toThrowError(/NG0600/);
374380
});
375381

382+
it('should pick custom CVA over default CVA when both are present', () => {
383+
@Component({
384+
selector: 'app-root',
385+
// Import ReactiveFormsModule to provide the non-standalone DefaultValueAccessor directive.
386+
// The selector for DefaultValueAccessor matches `[ngDefaultControl]`.
387+
imports: [FormField, CustomControl, ReactiveFormsModule],
388+
template: `<custom-control [formField]="f" ngDefaultControl />`,
389+
})
390+
class App {
391+
f = form<string>(signal(''));
392+
}
393+
394+
const fixture = act(() => TestBed.createComponent(App));
395+
const customControlInstance = fixture.debugElement.children[0].injector.get(CustomControl);
396+
397+
act(() => fixture.componentInstance.f().value.set('updated'));
398+
expect(customControlInstance.writeCount).toBe(2); // 1 initial + 1 update
399+
});
400+
376401
describe('properties', () => {
377402
describe('disabled', () => {
378403
it('should bind to directive input', () => {

packages/forms/src/directives/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export function syncPendingControls(
393393
// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
394394
export function selectValueAccessor(
395395
dir: NgControl,
396-
valueAccessors: ControlValueAccessor[] | null | undefined,
396+
valueAccessors: readonly ControlValueAccessor[] | null | undefined,
397397
): ControlValueAccessor | null {
398398
if (!valueAccessors) return null;
399399

packages/forms/src/forms.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export {
4848
SelectMultipleControlValueAccessor,
4949
ɵNgSelectMultipleOption,
5050
} from './directives/select_multiple_control_value_accessor';
51-
export {SetDisabledStateOption, ɵFORM_FIELD_PARSE_ERRORS} from './directives/shared';
51+
export {
52+
selectValueAccessor as ɵselectValueAccessor,
53+
SetDisabledStateOption,
54+
ɵFORM_FIELD_PARSE_ERRORS,
55+
} from './directives/shared';
5256
export {
5357
AsyncValidator,
5458
AsyncValidatorFn,

0 commit comments

Comments
 (0)