Skip to content

Commit 561772b

Browse files
leonsenftcrisbeto
authored andcommitted
fix(forms): allow custom controls to require dirty input
* Allow custom controls to make `dirty` a required input * Refactor test for `dirty` input to be consistent with other control properties * Test that `dirty` inputs are reset when the field binding changes (cherry picked from commit 89c37f1)
1 parent 929306e commit 561772b

File tree

2 files changed

+58
-28
lines changed

2 files changed

+58
-28
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ const formControlInputFields = [
4040
// Should be kept in sync with the `FormUiControl` bindings,
4141
// defined in `packages/forms/signals/src/api/control.ts`.
4242
'errors',
43-
'hidden',
44-
'invalid',
43+
'dirty',
4544
'disabled',
4645
'disabledReasons',
46+
'hidden',
47+
'invalid',
4748
'name',
4849
'readonly',
4950
'touched',

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

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,61 @@ describe('field directive', () => {
112112
});
113113

114114
describe('properties', () => {
115+
describe('dirty', () => {
116+
it('should bind to custom control', () => {
117+
@Component({
118+
selector: 'custom-control',
119+
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
120+
})
121+
class CustomControl implements FormValueControl<string> {
122+
readonly value = model.required<string>();
123+
readonly dirty = input.required<boolean>();
124+
}
125+
126+
@Component({
127+
template: ` <custom-control [field]="f" /> `,
128+
imports: [CustomControl, Field],
129+
})
130+
class TestCmp {
131+
readonly data = signal('');
132+
readonly f = form(this.data);
133+
readonly customControl = viewChild.required(CustomControl);
134+
}
135+
136+
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
137+
expect(comp.customControl().dirty()).toBe(false);
138+
act(() => comp.f().markAsDirty());
139+
expect(comp.customControl().dirty()).toBe(true);
140+
});
141+
142+
it('should be reset when field changes on custom control', () => {
143+
@Component({selector: 'custom-control', template: ``})
144+
class CustomControl implements FormValueControl<string> {
145+
readonly value = model.required<string>();
146+
readonly dirty = input.required<boolean>();
147+
}
148+
149+
@Component({
150+
imports: [Field, CustomControl],
151+
template: `<custom-control [field]="field()" />`,
152+
})
153+
class TestCmp {
154+
readonly f = form(signal({x: '', y: ''}));
155+
readonly field = signal(this.f.x);
156+
readonly customControl = viewChild.required(CustomControl);
157+
}
158+
159+
const fixture = act(() => TestBed.createComponent(TestCmp));
160+
const component = fixture.componentInstance;
161+
162+
act(() => component.f.x().markAsDirty());
163+
expect(component.customControl().dirty()).toBe(true);
164+
165+
act(() => component.field.set(component.f.y));
166+
expect(component.customControl().dirty()).toBe(false);
167+
});
168+
});
169+
115170
describe('disabled', () => {
116171
it('should bind to native control', () => {
117172
@Component({
@@ -2123,32 +2178,6 @@ describe('field directive', () => {
21232178
]);
21242179
});
21252180

2126-
it('should synchronize dirty status', () => {
2127-
@Component({
2128-
selector: 'my-input',
2129-
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
2130-
})
2131-
class CustomInput implements FormValueControl<string> {
2132-
value = model('');
2133-
dirty = input(false);
2134-
}
2135-
2136-
@Component({
2137-
template: ` <my-input [field]="f" /> `,
2138-
imports: [CustomInput, Field],
2139-
})
2140-
class DirtyTestCmp {
2141-
myInput = viewChild.required<CustomInput>(CustomInput);
2142-
data = signal('');
2143-
f = form(this.data);
2144-
}
2145-
2146-
const comp = act(() => TestBed.createComponent(DirtyTestCmp)).componentInstance;
2147-
expect(comp.myInput().dirty()).toBe(false);
2148-
act(() => comp.f().markAsDirty());
2149-
expect(comp.myInput().dirty()).toBe(true);
2150-
});
2151-
21522181
it('should synchronize pending status', async () => {
21532182
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
21542183

0 commit comments

Comments
 (0)