mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
test(forms): support directive composition with FormField
Support composing the `FormField` directive with custom controls:
This commit is contained in:
@@ -73,6 +73,40 @@ describe('FieldState focus behavior', () => {
|
||||
expect(document.activeElement).toBe(customControl.querySelector('input'));
|
||||
});
|
||||
|
||||
it('should delegate focus behavior to a custom control composed with a FormField', async () => {
|
||||
let focusCalled = false;
|
||||
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
template: '<input #input />',
|
||||
})
|
||||
class CustomControl {
|
||||
readonly value = model<string>();
|
||||
readonly input = viewChild.required<ElementRef<HTMLInputElement>>('input');
|
||||
focus() {
|
||||
focusCalled = true;
|
||||
this.input().nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''));
|
||||
}
|
||||
|
||||
const fixture = await act(() => TestBed.createComponent(TestCmp));
|
||||
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
|
||||
|
||||
await act(() => fixture.componentInstance.f().focusBoundControl());
|
||||
expect(focusCalled).toBeTrue();
|
||||
expect(document.activeElement).not.toBe(customControl);
|
||||
expect(document.activeElement).toBe(customControl.querySelector('input'));
|
||||
});
|
||||
|
||||
it('should directly focus a custom control that has no custom focus logic', async () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
|
||||
@@ -207,6 +207,33 @@ describe('field directive', () => {
|
||||
expect(comp.customControl().dirty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly dirty = input.required<boolean>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal('');
|
||||
readonly f = form(this.data);
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
|
||||
expect(comp.customControl().dirty()).toBe(false);
|
||||
act(() => comp.f().markAsDirty());
|
||||
expect(comp.customControl().dirty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to directive input on native control', () => {
|
||||
@Directive({selector: '[testDir]'})
|
||||
class TestDir {
|
||||
@@ -385,6 +412,35 @@ describe('field directive', () => {
|
||||
expect(component.customControl().disabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<boolean> {
|
||||
readonly value = model(false);
|
||||
readonly disabled = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly disabled = signal(false);
|
||||
readonly f = form(signal(false), (p) => {
|
||||
disabled(p, this.disabled);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
|
||||
expect(comp.customControl().disabled()).toBe(false);
|
||||
act(() => comp.disabled.set(true));
|
||||
expect(comp.customControl().disabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<boolean> {
|
||||
@@ -618,6 +674,37 @@ describe('field directive', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly disabledReasons =
|
||||
input.required<readonly WithOptionalFieldTree<DisabledReason>[]>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal('');
|
||||
readonly f = form(this.data, (p) => {
|
||||
disabled(p, () => 'Currently unavailable');
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
|
||||
|
||||
expect(comp.customControl().disabledReasons()).toEqual([
|
||||
{message: 'Currently unavailable', fieldTree: comp.f},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
@@ -788,6 +875,36 @@ describe('field directive', () => {
|
||||
expect(comp.customControl().errors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly errors = input.required<readonly WithOptionalFieldTree<ValidationError>[]>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal('');
|
||||
readonly f = form(this.data, (p) => {
|
||||
required(p);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
|
||||
expect(comp.customControl().errors()).toEqual([requiredError({fieldTree: comp.f})]);
|
||||
|
||||
act(() => comp.f().value.set('valid'));
|
||||
expect(comp.customControl().errors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
@@ -949,6 +1066,39 @@ describe('field directive', () => {
|
||||
expect(component.customControl().hidden()).toBe(false);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly hidden = input.required<boolean>();
|
||||
}
|
||||
|
||||
const visible = signal(false);
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="field()" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(''), (p) => {
|
||||
hidden(p, () => !visible());
|
||||
});
|
||||
readonly field = signal(this.f);
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().hidden()).toBe(true);
|
||||
|
||||
act(() => visible.set(true));
|
||||
expect(component.customControl().hidden()).toBe(false);
|
||||
});
|
||||
|
||||
it('should bind to a custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -1145,6 +1295,35 @@ describe('field directive', () => {
|
||||
expect(comp.customControl().invalid()).toBe(false);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly invalid = input.required<boolean>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal('');
|
||||
readonly f = form(this.data, (p) => {
|
||||
required(p);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const comp = act(() => TestBed.createComponent(TestCmp)).componentInstance;
|
||||
expect(comp.customControl().invalid()).toBe(true);
|
||||
act(() => comp.f().value.set('valid'));
|
||||
expect(comp.customControl().invalid()).toBe(false);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
@@ -1324,6 +1503,38 @@ describe('field directive', () => {
|
||||
expect(fixture.nativeElement.innerText).toBe('ab');
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '{{ value() }}',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly name = input('');
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `
|
||||
@for (item of f; track item) {
|
||||
<custom-control [formField]="item" />
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly f = form(signal(['a', 'b']), {name: 'root'});
|
||||
readonly controls = viewChildren(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
const control0 = component.controls()[0];
|
||||
const control1 = component.controls()[1];
|
||||
expect(control0.name()).toBe('root.0');
|
||||
expect(control1.name()).toBe('root.1');
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: `{{ value() }}`})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -1473,6 +1684,52 @@ describe('field directive', () => {
|
||||
expect(comp.customControl().pending()).toBe(false);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', async () => {
|
||||
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
|
||||
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model.required<string>();
|
||||
readonly pending = input.required<boolean>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: ` <custom-control [formField]="f" /> `,
|
||||
imports: [CustomControl],
|
||||
})
|
||||
class TestCmp {
|
||||
readonly data = signal('test');
|
||||
readonly f = form(this.data, (p) => {
|
||||
validateAsync(p, {
|
||||
params: () => [],
|
||||
factory: (params) =>
|
||||
resource({
|
||||
params,
|
||||
loader: () => promise,
|
||||
}),
|
||||
onSuccess: (results) => results,
|
||||
onError: () => null,
|
||||
});
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const comp = fixture.componentInstance;
|
||||
|
||||
expect(comp.customControl().pending()).toBe(true);
|
||||
|
||||
resolve([]);
|
||||
await promise;
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(comp.customControl().pending()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be reset when field changes on custom control', async () => {
|
||||
const {promise, resolve} = promiseWithResolvers<ValidationError[]>();
|
||||
|
||||
@@ -1656,6 +1913,37 @@ describe('field directive', () => {
|
||||
expect(component.child().readonly()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly readonly = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly readonly = signal(false);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
readonly(p, this.readonly);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().readonly()).toBe(false);
|
||||
|
||||
act(() => component.readonly.set(true));
|
||||
expect(component.customControl().readonly()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -1877,6 +2165,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().required()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly required = input(false);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly required = signal(false);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
required(p, {when: this.required});
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().required()).toBe(false);
|
||||
|
||||
act(() => component.required.set(true));
|
||||
expect(component.customControl().required()).toBe(true);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -2098,6 +2417,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().max()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
readonly max = input<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly max = signal(10);
|
||||
readonly f = form(signal(5), (p) => {
|
||||
max(p, this.max);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().max()).toBe(10);
|
||||
|
||||
act(() => component.max.set(5));
|
||||
expect(component.customControl().max()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
@@ -2319,6 +2669,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().min()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
readonly value = model(0);
|
||||
readonly min = input<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly min = signal(10);
|
||||
readonly f = form(signal(15), (p) => {
|
||||
min(p, this.min);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().min()).toBe(10);
|
||||
|
||||
act(() => component.min.set(5));
|
||||
expect(component.customControl().min()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<number> {
|
||||
@@ -2540,6 +2921,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().maxLength()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly maxLength = input<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly maxLength = signal(10);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
maxLength(p, this.maxLength);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().maxLength()).toBe(10);
|
||||
|
||||
act(() => component.maxLength.set(5));
|
||||
expect(component.customControl().maxLength()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -2777,6 +3189,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().minLength()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly minLength = input<number>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly minLength = signal(10);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
minLength(p, this.minLength);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().minLength()).toBe(10);
|
||||
|
||||
act(() => component.minLength.set(5));
|
||||
expect(component.customControl().minLength()).toBe(5);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -2994,6 +3437,37 @@ describe('field directive', () => {
|
||||
expect(component.customControl().pattern()).toEqual([/def/]);
|
||||
});
|
||||
|
||||
it('should bind to a custom control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'custom-control',
|
||||
template: '',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
readonly value = model('');
|
||||
readonly pattern = input<readonly RegExp[]>([]);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomControl],
|
||||
template: `<custom-control [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
readonly pattern = signal(/abc/);
|
||||
readonly f = form(signal(''), (p) => {
|
||||
pattern(p, this.pattern);
|
||||
});
|
||||
readonly customControl = viewChild.required(CustomControl);
|
||||
}
|
||||
|
||||
const fixture = act(() => TestBed.createComponent(TestCmp));
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.customControl().pattern()).toEqual([/abc/]);
|
||||
|
||||
act(() => component.pattern.set(/def/));
|
||||
expect(component.customControl().pattern()).toEqual([/def/]);
|
||||
});
|
||||
|
||||
it('should bind to custom control', () => {
|
||||
@Component({selector: 'custom-control', template: ``})
|
||||
class CustomControl implements FormValueControl<string> {
|
||||
@@ -3448,6 +3922,43 @@ describe('field directive', () => {
|
||||
expect(cmp.f().value()).toBe('typing');
|
||||
});
|
||||
|
||||
it('synchronizes with a custom value control when composed as a host directive', () => {
|
||||
@Component({
|
||||
selector: 'my-input',
|
||||
template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
|
||||
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
||||
})
|
||||
class CustomInput implements FormValueControl<string> {
|
||||
value = model('');
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [CustomInput],
|
||||
template: `<my-input [formField]="f" />`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form<string>(signal('test'));
|
||||
}
|
||||
|
||||
const fix = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fix.nativeElement.firstChild.firstChild as HTMLInputElement;
|
||||
const cmp = fix.componentInstance as TestCmp;
|
||||
|
||||
// Initial state
|
||||
expect(input.value).toBe('test');
|
||||
|
||||
// Model -> View
|
||||
act(() => cmp.f().value.set('testing'));
|
||||
expect(input.value).toBe('testing');
|
||||
|
||||
// View -> Model
|
||||
act(() => {
|
||||
input.value = 'typing';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
});
|
||||
expect(cmp.f().value()).toBe('typing');
|
||||
});
|
||||
|
||||
it('synchronizes with a custom value control directive', () => {
|
||||
@Directive({
|
||||
selector: 'input[my-input]',
|
||||
|
||||
Reference in New Issue
Block a user