Skip to content

Commit 3529877

Browse files
leonsenftthePunderWoman
authored andcommitted
fix(forms): mark field as dirty when value is changed by a bound control (#64483)
Fix #64465. Fix #63623. PR Close #64483
1 parent 348a8f3 commit 3529877

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-3
lines changed

packages/core/src/render3/instructions/control.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ function listenToCustomControl(
221221
outputName,
222222
outputName,
223223
wrapListener(tNode, lView, (newValue: unknown) => {
224-
control.state().value.set(newValue);
224+
const state = control.state();
225+
state.value.set(newValue);
226+
state.markAsDirty();
225227
}),
226228
);
227229

@@ -279,8 +281,9 @@ function listenToNativeControl(lView: LView<{} | null>, tNode: TNode, control:
279281
const renderer = lView[RENDERER];
280282
const inputListener = () => {
281283
const element = getNativeByTNode(tNode, lView) as NativeControlElement;
282-
const value = control.state().value;
283-
value.set(getNativeControlValue(element, value));
284+
const state = control.state();
285+
state.value.set(getNativeControlValue(element, state.value));
286+
state.markAsDirty();
284287
};
285288
listenToDomEvent(
286289
tNode,

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,62 @@ describe('field directive', () => {
12831283
});
12841284
});
12851285

1286+
describe('should be marked dirty by user interaction', () => {
1287+
it('native control', () => {
1288+
@Component({
1289+
imports: [Field],
1290+
template: `<input [field]="f">`,
1291+
})
1292+
class TestCmp {
1293+
f = form(signal(''));
1294+
}
1295+
1296+
const fixture = act(() => TestBed.createComponent(TestCmp));
1297+
const input = fixture.nativeElement.firstChild as HTMLInputElement;
1298+
const field = fixture.componentInstance.f;
1299+
1300+
expect(field().dirty()).toBe(false);
1301+
1302+
act(() => {
1303+
input.value = 'typing';
1304+
input.dispatchEvent(new Event('input'));
1305+
});
1306+
1307+
expect(field().dirty()).toBe(true);
1308+
});
1309+
1310+
it('custom control', () => {
1311+
@Component({
1312+
selector: 'my-input',
1313+
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
1314+
})
1315+
class CustomInput implements FormValueControl<string> {
1316+
value = model('');
1317+
}
1318+
1319+
@Component({
1320+
imports: [Field, CustomInput],
1321+
template: `<my-input [field]="f" />`,
1322+
})
1323+
class TestCmp {
1324+
f = form<string>(signal(''));
1325+
}
1326+
1327+
const fixture = act(() => TestBed.createComponent(TestCmp));
1328+
const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
1329+
const field = fixture.componentInstance.f;
1330+
1331+
expect(field().dirty()).toBe(false);
1332+
1333+
act(() => {
1334+
input.value = 'typing';
1335+
input.dispatchEvent(new Event('input'));
1336+
});
1337+
1338+
expect(field().dirty()).toBe(true);
1339+
});
1340+
});
1341+
12861342
it('should throw for invalid field directive host', () => {
12871343
@Component({
12881344
imports: [Field],

0 commit comments

Comments
 (0)