mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(forms): add signals for dirty, hidden, and pending states in custom controls
FormUiControl states that hidden, pending and dirty will be bind in custom controls, but this is currently not the case.
Fixes #65575
(cherry picked from commit 9fe9566581)
This commit is contained in:
parent
288238abef
commit
55fc677cef
3 changed files with 123 additions and 0 deletions
|
|
@ -930,7 +930,9 @@ type ControlBindings = {
|
|||
const CONTROL_BINDING_NAMES = {
|
||||
disabled: 'disabled',
|
||||
disabledReasons: 'disabledReasons',
|
||||
dirty: 'dirty',
|
||||
errors: 'errors',
|
||||
hidden: 'hidden',
|
||||
invalid: 'invalid',
|
||||
max: 'max',
|
||||
maxLength: 'maxLength',
|
||||
|
|
@ -938,6 +940,7 @@ const CONTROL_BINDING_NAMES = {
|
|||
minLength: 'minLength',
|
||||
name: 'name',
|
||||
pattern: 'pattern',
|
||||
pending: 'pending',
|
||||
readonly: 'readonly',
|
||||
required: 'required',
|
||||
touched: 'touched',
|
||||
|
|
|
|||
|
|
@ -143,6 +143,21 @@ export interface ɵFieldState<T> {
|
|||
*/
|
||||
readonly touched: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* A signal indicating whether the field value has been changed by user.
|
||||
*/
|
||||
readonly dirty: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* A signal indicating whether the field is hidden.
|
||||
*/
|
||||
readonly hidden: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* A signal indicating whether there are any validators still pending for this field.
|
||||
*/
|
||||
readonly pending: Signal<boolean>;
|
||||
|
||||
/**
|
||||
* A writable signal containing the value for this field.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
inputBinding,
|
||||
model,
|
||||
numberAttribute,
|
||||
resource,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
|
|
@ -39,6 +40,7 @@ import {
|
|||
provideSignalFormsConfig,
|
||||
readonly,
|
||||
required,
|
||||
validateAsync,
|
||||
type DisabledReason,
|
||||
type FieldTree,
|
||||
type FormCheckboxControl,
|
||||
|
|
@ -1886,6 +1888,109 @@ describe('field directive', () => {
|
|||
expect(comp.myInput().invalid()).toBe(false);
|
||||
});
|
||||
|
||||
it('should synchronize hidden status', () => {
|
||||
@Component({
|
||||
selector: 'my-input',
|
||||
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
|
||||
})
|
||||
class CustomInput implements FormValueControl<string> {
|
||||
value = model('');
|
||||
hidden = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<my-input [field]="f" />
|
||||
`,
|
||||
imports: [CustomInput, Field],
|
||||
})
|
||||
class HiddenTestCmp {
|
||||
myInput = viewChild.required<CustomInput>(CustomInput);
|
||||
data = signal('');
|
||||
f = form(this.data, (p) => {
|
||||
hidden(p, ({value}) => value() === '');
|
||||
});
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(HiddenTestCmp)).componentInstance;
|
||||
expect(comp.myInput().hidden()).toBe(true);
|
||||
act(() => comp.f().value.set('visible'));
|
||||
expect(comp.myInput().hidden()).toBe(false);
|
||||
});
|
||||
|
||||
it('should synchronize dirty status', () => {
|
||||
@Component({
|
||||
selector: 'my-input',
|
||||
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
|
||||
})
|
||||
class CustomInput implements FormValueControl<string> {
|
||||
value = model('');
|
||||
dirty = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<my-input [field]="f" />
|
||||
`,
|
||||
imports: [CustomInput, Field],
|
||||
})
|
||||
class DirtyTestCmp {
|
||||
myInput = viewChild.required<CustomInput>(CustomInput);
|
||||
data = signal('');
|
||||
f = form(this.data);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(DirtyTestCmp)).componentInstance;
|
||||
expect(comp.myInput().dirty()).toBe(false);
|
||||
act(() => comp.f().markAsDirty());
|
||||
expect(comp.myInput().dirty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should synchronize pending status', async () => {
|
||||
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
|
||||
|
||||
@Component({
|
||||
selector: 'my-input',
|
||||
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
|
||||
})
|
||||
class CustomInput implements FormValueControl<string> {
|
||||
value = model('');
|
||||
pending = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<my-input [field]="f" />
|
||||
`,
|
||||
imports: [CustomInput, Field],
|
||||
})
|
||||
class PendingTestCmp {
|
||||
myInput = viewChild.required<CustomInput>(CustomInput);
|
||||
data = signal('test');
|
||||
f = form(this.data, (p) => {
|
||||
validateAsync(p, {
|
||||
params: () => [],
|
||||
factory: (params) =>
|
||||
resource({
|
||||
params,
|
||||
loader: () => promise,
|
||||
}),
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const fix = act(() => TestBed.createComponent(PendingTestCmp));
|
||||
|
||||
expect(fix.componentInstance.myInput().pending()).toBe(true);
|
||||
|
||||
resolve([]);
|
||||
await promise;
|
||||
await fix.whenStable();
|
||||
expect(fix.componentInstance.myInput().pending()).toBe(false);
|
||||
});
|
||||
|
||||
it(`should mark field as touched on native control 'blur' event`, () => {
|
||||
@Component({
|
||||
imports: [Field],
|
||||
|
|
|
|||
Loading…
Reference in a new issue