Skip to content

Commit 505bde1

Browse files
leonsenftthePunderWoman
authored andcommitted
fix(forms): mark field as dirty when value is changed by ControlValueAccessor (#64471)
This corresponds with fixes made for native and custom signal form controls: #64483. PR Close #64471
1 parent a0f3960 commit 505bde1

File tree

2 files changed

+33
-6
lines changed

2 files changed

+33
-6
lines changed

packages/forms/signals/src/api/field_directive.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const FIELD = new InjectionToken<Field<unknown>>(
3333
* Binds a form `FieldTree` to a UI control that edits it. A UI control can be one of several things:
3434
* 1. A native HTML input or textarea
3535
* 2. A signal forms custom control that implements `FormValueControl` or `FormCheckboxControl`
36-
* 3. A component that provides a `ControlValueAccessor`. This should only be used to backwards
36+
* 3. A component that provides a `ControlValueAccessor`. This should only be used for backwards
3737
* compatibility with reactive forms. Prefer options (1) and (2).
3838
*
3939
* This directive has several responsibilities:
@@ -83,7 +83,11 @@ export class Field<T> implements ɵControl<T> {
8383

8484
ɵinteropControlCreate() {
8585
const controlValueAccessor = this.controlValueAccessor!;
86-
controlValueAccessor.registerOnChange((value: T) => this.state().value.set(value));
86+
controlValueAccessor.registerOnChange((value: T) => {
87+
const state = this.state();
88+
state.value.set(value);
89+
state.markAsDirty();
90+
});
8791
controlValueAccessor.registerOnTouched(() => this.state().markAsTouched());
8892
}
8993

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('ControlValueAccessor', () => {
7575

7676
const fixture = act(() => TestBed.createComponent(TestCmp));
7777
const control = fixture.componentInstance.control;
78-
const input = fixture.nativeElement.firstChild.firstChild;
78+
const input = fixture.nativeElement.querySelector('input');
7979

8080
// Initial state
8181
expect(control().value).toBe('test');
@@ -92,6 +92,29 @@ describe('ControlValueAccessor', () => {
9292
expect(fixture.componentInstance.f().value()).toBe('typing');
9393
});
9494

95+
it('should mark field dirty on changes', () => {
96+
@Component({
97+
imports: [Field, CustomControl],
98+
template: `<custom-control [field]="f" />`,
99+
})
100+
class TestCmp {
101+
f = form<string>(signal(''));
102+
}
103+
104+
const fixture = act(() => TestBed.createComponent(TestCmp));
105+
const input = fixture.nativeElement.querySelector('input');
106+
const field = fixture.componentInstance.f;
107+
108+
expect(field().dirty()).toBe(false);
109+
110+
act(() => {
111+
input.value = 'typing';
112+
input.dispatchEvent(new Event('input'));
113+
});
114+
115+
expect(field().dirty()).toBe(true);
116+
});
117+
95118
it('should propagate touched events to field', () => {
96119
@Component({
97120
imports: [Field, CustomControl],
@@ -102,7 +125,7 @@ describe('ControlValueAccessor', () => {
102125
}
103126

104127
const fixture = act(() => TestBed.createComponent(TestCmp));
105-
const input = fixture.nativeElement.firstChild.firstChild;
128+
const input = fixture.nativeElement.querySelector('input');
106129
expect(fixture.componentInstance.f().touched()).toBe(false);
107130

108131
act(() => input.dispatchEvent(new Event('blur')));
@@ -122,7 +145,7 @@ describe('ControlValueAccessor', () => {
122145
}
123146

124147
const fixture = act(() => TestBed.createComponent(TestCmp));
125-
const input = fixture.nativeElement.firstChild.firstChild;
148+
const input = fixture.nativeElement.querySelector('input');
126149
expect(input.disabled).toBe(false);
127150

128151
act(() => enabled.set(false));
@@ -181,7 +204,7 @@ describe('ControlValueAccessor', () => {
181204

182205
const fixture = act(() => TestBed.createComponent(TestCmp));
183206
const control = fixture.componentInstance.control;
184-
const input = fixture.nativeElement.firstChild.firstChild;
207+
const input = fixture.nativeElement.querySelector('input');
185208

186209
// Initial state
187210
expect(control().value).toBe('test');

0 commit comments

Comments
 (0)