我可以在 Angular 2+ 中访问自定义 ControlValueAccessor 的 formControl 吗?
Can I access to formControl of my custom ControlValueAccessor in Angular 2+?
我想在 Angular 2+ 中创建一个带有 ControlValueAccessor 接口的自定义表单元素。该元素将是 <select>
的包装器。是否可以将 formControl 属性传播到包装元素?在我的例子中,验证状态没有传播到嵌套 select,如您在随附的屏幕截图中所见。
我的组件可用如下:
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html'
})
export class OptionsComponent implements ControlValueAccessor, OnInit {
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
这是我的组件模板:
<select class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
我看到两个选项:
- 每当
<select>
FormControl
值发生变化时,将错误从组件 FormControl
传播到 <select>
FormControl
- 将验证器从组件
FormControl
传播到 <select>
FormControl
以下变量可用:
selectModel
是 <select>
的 NgModel
formControl
是作为参数接收的组件的 FormControl
选项 1:传播错误
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
选项 2:传播验证器
ngAfterViewInit(): void {
this.selectModel.control.setValidators(this.formControl.validator);
this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
}
两者之间的区别在于传播错误意味着已经有错误,而秒选项涉及第二次执行验证器。其中一些,例如异步验证器,可能执行起来成本太高。
传播所有属性?
没有传播所有属性的通用解决方案。各种属性由各种指令或其他方式设置,因此具有不同的生命周期,这意味着需要特定的处理。当前的解决方案涉及传播验证错误和验证器。那里有很多可用的属性。
请注意,通过订阅 FormControl.statusChanges()
,您可能会从 FormControl
实例中获得不同的状态更改。这样你就可以得到控件是VALID
、INVALID
、DISABLED
还是PENDING
(异步验证仍然是运行)。
验证是如何进行的?
在引擎盖下,验证器是使用指令 (check the source code) 应用的。指令有 providers: [REQUIRED_VALIDATOR]
这意味着自己的分层注入器用于注册该验证器实例。因此,根据应用于元素的属性,指令将在与目标元素关联的注入器上添加验证器实例。
接下来,这些验证器由 NgModel
and FormControlDirective
检索。
验证器和值访问器的检索方式如下:
constructor(@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
分别为:
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[])
请注意,使用了 @Self()
,因此使用自己的注入器(指令应用到的元素)来获取依赖项。
NgModel
和 FormControlDirective
有一个 FormControl
的实例,它实际更新值并执行验证器。
因此,主要的交互点是 FormControl
实例。
此外,所有验证器或值访问器都在应用它们的元素的注入器中注册。这意味着 parent 不应访问该注入器。因此,从当前组件访问 <select>
.
提供的注入器是一种不好的做法
选项 1 的示例代码(可轻松替换为选项 2)
以下示例有两个验证器:一个是必需的,另一个是强制选项匹配 "option 3".
的模式
options.component.ts
import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html',
styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ViewChild('selectModel') selectModel: NgModel;
@Input() formControl: FormControl;
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
options.component.html
<select #selectModel="ngModel"
class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
options.component.scss
:host {
display: inline-block;
border: 5px solid transparent;
&.ng-invalid {
border-color: purple;
}
select {
border: 5px solid transparent;
&.ng-invalid {
border-color: red;
}
}
}
用法
定义 FormControl
实例:
export class AppComponent implements OnInit {
public control: FormControl;
constructor() {
this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
}
...
将 FormControl
实例绑定到组件:
<inf-select name="myName" [formControl]="control"></inf-select>
虚拟设置服务
/**
* TODO remove this class, added just to make injection work
*/
export class SettingsService {
public getOption(name: string): [{ description: string }] {
return [
{ description: 'option 1' },
{ description: 'option 2' },
{ description: 'option 3' },
{ description: 'option 4' },
{ description: 'option 5' },
];
}
}
我认为这是在基于 ControlValueAccessor
的组件中访问 FormControl 的最干净的解决方案。解决方案基于提及的内容 here in Angular Material documentation.
// parent component template
<my-text-input formControlName="name"></my-text-input>
@Component({
selector: 'my-text-input',
template: '<input
type="text"
[value]="value"
/>',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor {
// Here is missing standard stuff to implement ControlValueAccessor interface
constructor(@Optional() @Self() public ngControl: NgControl) {
if (ngControl != null) {
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
ngControl.valueAccessor = this;
}
}
ngAfterContentInit(): void {
const control = this.ngControl && this.ngControl.control;
if (control) {
// FormControl should be available here
}
}
}
如果您实施验证 (Validator / NG_VALIDATORS),AbstractControl 会很早地传递到您的验证函数中。你可以把它藏起来。
validate(c: AbstractControl): ValidationErrors {
this.myControl = c;
这是一个示例,展示了如何获取(和 re-use)底层 FormControl 和底层 ControlValueAccessor。
这在包装组件(如输入)时很有用,因为您可以 re-use angular 创建的现有 FormControl 和 ControlValueAccessor,这样您就不必 re-implement它。
@Component({
selector: 'resettable-input',
template: `
<input type="text" [formControl]="control">
<button (click)="clearInput()">clear</button>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: ResettableInputComponent,
multi: true
}]
})
export class ResettableInputComponent implements ControlValueAccessor {
@ViewChild(FormControlDirective, {static: true}) formControlDirective: FormControlDirective;
@Input() formControl: FormControl;
@Input() formControlName: string;
// get hold of FormControl instance no matter formControl or formControlName is given.
// If formControlName is given, then this.controlContainer.control is the parent FormGroup (or FormArray) instance.
get control() {
return this.formControl || this.controlContainer.control.get(this.formControlName);
}
constructor(private controlContainer: ControlContainer) { }
clearInput() {
this.control.setValue('');
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor.writeValue(obj);
}
setDisabledState(isDisabled: boolean): void {
this.formControlDirective.valueAccessor.setDisabledState(isDisabled);
}
}
我想在 Angular 2+ 中创建一个带有 ControlValueAccessor 接口的自定义表单元素。该元素将是 <select>
的包装器。是否可以将 formControl 属性传播到包装元素?在我的例子中,验证状态没有传播到嵌套 select,如您在随附的屏幕截图中所见。
我的组件可用如下:
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html'
})
export class OptionsComponent implements ControlValueAccessor, OnInit {
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
这是我的组件模板:
<select class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
我看到两个选项:
- 每当
<select>
FormControl
值发生变化时,将错误从组件FormControl
传播到<select>
FormControl
- 将验证器从组件
FormControl
传播到<select>
FormControl
以下变量可用:
selectModel
是<select>
的 formControl
是作为参数接收的组件的FormControl
NgModel
选项 1:传播错误
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
选项 2:传播验证器
ngAfterViewInit(): void {
this.selectModel.control.setValidators(this.formControl.validator);
this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
}
两者之间的区别在于传播错误意味着已经有错误,而秒选项涉及第二次执行验证器。其中一些,例如异步验证器,可能执行起来成本太高。
传播所有属性?
没有传播所有属性的通用解决方案。各种属性由各种指令或其他方式设置,因此具有不同的生命周期,这意味着需要特定的处理。当前的解决方案涉及传播验证错误和验证器。那里有很多可用的属性。
请注意,通过订阅 FormControl.statusChanges()
,您可能会从 FormControl
实例中获得不同的状态更改。这样你就可以得到控件是VALID
、INVALID
、DISABLED
还是PENDING
(异步验证仍然是运行)。
验证是如何进行的?
在引擎盖下,验证器是使用指令 (check the source code) 应用的。指令有 providers: [REQUIRED_VALIDATOR]
这意味着自己的分层注入器用于注册该验证器实例。因此,根据应用于元素的属性,指令将在与目标元素关联的注入器上添加验证器实例。
接下来,这些验证器由 NgModel
and FormControlDirective
检索。
验证器和值访问器的检索方式如下:
constructor(@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
分别为:
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[])
请注意,使用了 @Self()
,因此使用自己的注入器(指令应用到的元素)来获取依赖项。
NgModel
和 FormControlDirective
有一个 FormControl
的实例,它实际更新值并执行验证器。
因此,主要的交互点是 FormControl
实例。
此外,所有验证器或值访问器都在应用它们的元素的注入器中注册。这意味着 parent 不应访问该注入器。因此,从当前组件访问 <select>
.
选项 1 的示例代码(可轻松替换为选项 2)
以下示例有两个验证器:一个是必需的,另一个是强制选项匹配 "option 3".
的模式options.component.ts
import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html',
styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ViewChild('selectModel') selectModel: NgModel;
@Input() formControl: FormControl;
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
options.component.html
<select #selectModel="ngModel"
class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
options.component.scss
:host {
display: inline-block;
border: 5px solid transparent;
&.ng-invalid {
border-color: purple;
}
select {
border: 5px solid transparent;
&.ng-invalid {
border-color: red;
}
}
}
用法
定义 FormControl
实例:
export class AppComponent implements OnInit {
public control: FormControl;
constructor() {
this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
}
...
将 FormControl
实例绑定到组件:
<inf-select name="myName" [formControl]="control"></inf-select>
虚拟设置服务
/**
* TODO remove this class, added just to make injection work
*/
export class SettingsService {
public getOption(name: string): [{ description: string }] {
return [
{ description: 'option 1' },
{ description: 'option 2' },
{ description: 'option 3' },
{ description: 'option 4' },
{ description: 'option 5' },
];
}
}
我认为这是在基于 ControlValueAccessor
的组件中访问 FormControl 的最干净的解决方案。解决方案基于提及的内容 here in Angular Material documentation.
// parent component template
<my-text-input formControlName="name"></my-text-input>
@Component({
selector: 'my-text-input',
template: '<input
type="text"
[value]="value"
/>',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor {
// Here is missing standard stuff to implement ControlValueAccessor interface
constructor(@Optional() @Self() public ngControl: NgControl) {
if (ngControl != null) {
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
ngControl.valueAccessor = this;
}
}
ngAfterContentInit(): void {
const control = this.ngControl && this.ngControl.control;
if (control) {
// FormControl should be available here
}
}
}
如果您实施验证 (Validator / NG_VALIDATORS),AbstractControl 会很早地传递到您的验证函数中。你可以把它藏起来。
validate(c: AbstractControl): ValidationErrors {
this.myControl = c;
这是一个示例,展示了如何获取(和 re-use)底层 FormControl 和底层 ControlValueAccessor。
这在包装组件(如输入)时很有用,因为您可以 re-use angular 创建的现有 FormControl 和 ControlValueAccessor,这样您就不必 re-implement它。
@Component({
selector: 'resettable-input',
template: `
<input type="text" [formControl]="control">
<button (click)="clearInput()">clear</button>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: ResettableInputComponent,
multi: true
}]
})
export class ResettableInputComponent implements ControlValueAccessor {
@ViewChild(FormControlDirective, {static: true}) formControlDirective: FormControlDirective;
@Input() formControl: FormControl;
@Input() formControlName: string;
// get hold of FormControl instance no matter formControl or formControlName is given.
// If formControlName is given, then this.controlContainer.control is the parent FormGroup (or FormArray) instance.
get control() {
return this.formControl || this.controlContainer.control.get(this.formControlName);
}
constructor(private controlContainer: ControlContainer) { }
clearInput() {
this.control.setValue('');
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor.writeValue(obj);
}
setDisabledState(isDisabled: boolean): void {
this.formControlDirective.valueAccessor.setDisabledState(isDisabled);
}
}