Angular 2 - 组件内的 formControlName
Angular 2 - formControlName inside component
我想创建一个可以与 FormBuilder 一起使用的自定义输入组件 API。如何在组件中添加 formControlName
?
模板:
<label class="custom-input__label"
*ngIf="label">
{{ label }}
</label>
<input class="custom-input__input"
placeholder="{{ placeholder }}"
name="title" />
<span class="custom-input__message"
*ngIf="message">
{{ message }}
</span>
组件:
import {
Component,
Input,
ViewEncapsulation
} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'custom-input',
host: {
'[class.custom-input]': 'true'
},
templateUrl: 'input.component.html',
styleUrls: ['input.component.css'],
encapsulation: ViewEncapsulation.None,
})
export class InputComponent {
@Input() label: string;
@Input() message: string;
@Input() placeholder: string;
}
用法:
<custom-input label="Title"
formControlName="title" // Pass this to input inside the component>
</custom-input>
这里的主要思想是你必须 link FormControl 到 FormGroup,这可以通过将 FormGroup 传递到每个输入组件来完成...
因此您的输入模板可能如下所示:
<div [formGroup]="form">
<label *ngIf="label">{{ label }}</label>
<input [formControlName]="inputName" />
<span *ngIf="message">{{ message }}</span>
</div>
输入组件的 @Input
将是 form
、label
、inputName
和 message
。
可以这样使用:
<form [FormGroup]="yourFormGroup">
<custom-input
[form]="yourFormGroup"
[inputName]="thisFormControlName"
[message]="yourMessage"
[label]="yourLabel">
</custom-input>
</form>
有关自定义表单输入组件的更多信息,我建议您查看 Angular's Dynamic Forms。
此外,如果您想了解有关如何使 @Input
和 @Output
正常工作的更多信息,请查看 Angular Docs Here
您不应将 formControlName
属性添加到自定义组件模板中的输入字段。
根据最佳实践,您应该在自定义输入元素本身上添加 formControlName
。
在这里,您可以在自定义输入组件中使用的是 controlValueAccessor
界面,只要您的自定义输入模板中的输入字段事件发生更改,您的自定义输入就会更新值或模糊。
它在您的自定义输入的表单控件行为与您为该自定义表单控件提供的 UI 之间提供连接(以更新值或其他需求)。
下面是TypeScript自定义输入组件的代码
import { Component, Input, forwardRef, AfterViewInit, trigger, state, animate, transition, style, HostListener, OnChanges, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl } from '@angular/forms';
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
};
@Component({
selector: 'inv-input',
templateUrl:'./input-text.component.html',
styleUrls: ['./input-text.component.css'],
encapsulation: ViewEncapsulation.None,
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR],
animations:[trigger(
'visibilityChanged',[
state('true',style({'height':'*','padding-top':'4px'})),
state('false',style({height:'0px','padding-top':'0px'})),
transition('*=>*',animate('200ms'))
]
)]
})
export class InputComponent implements ControlValueAccessor, AfterViewInit, OnChanges {
// Input field type eg:text,password
@Input() type = "text";
// ID attribute for the field and for attribute for the label
@Input() idd = "";
// The field name text . used to set placeholder also if no pH (placeholder) input is given
@Input() text = "";
// placeholder input
@Input() pH:string;
//current form control input. helpful in validating and accessing form control
@Input() c:FormControl = new FormControl();
// set true if we need not show the asterisk in red color
@Input() optional : boolean = false;
//@Input() v:boolean = true; // validation input. if false we will not show error message.
// errors for the form control will be stored in this array
errors:Array<any> = ['This field is required'];
// get reference to the input element
@ViewChild('input') inputRef:ElementRef;
constructor() {
}
ngOnChanges(){
}
//Lifecycle hook. angular.io for more info
ngAfterViewInit(){
// set placeholder default value when no input given to pH property
if(this.pH === undefined){
this.pH = "Enter "+this.text;
}
// RESET the custom input form control UI when the form control is RESET
this.c.valueChanges.subscribe(
() => {
// check condition if the form control is RESET
if (this.c.value == "" || this.c.value == null || this.c.value == undefined) {
this.innerValue = "";
this.inputRef.nativeElement.value = "";
}
}
);
}
//The internal data model for form control value access
private innerValue: any = '';
// event fired when input value is changed . later propagated up to the form control using the custom value accessor interface
onChange(e:Event, value:any){
//set changed value
this.innerValue = value;
// propagate value into form control using control value accessor interface
this.propagateChange(this.innerValue);
//reset errors
this.errors = [];
//setting, resetting error messages into an array (to loop) and adding the validation messages to show below the field area
for (var key in this.c.errors) {
if (this.c.errors.hasOwnProperty(key)) {
if(key === "required"){
this.errors.push("This field is required");
}else{
this.errors.push(this.c.errors[key]);
}
}
}
}
//get accessor
get value(): any {
return this.innerValue;
};
//set accessor including call the onchange callback
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
}
}
//propagate changes into the custom form control
propagateChange = (_: any) => { }
//From ControlValueAccessor interface
writeValue(value: any) {
this.innerValue = value;
}
//From ControlValueAccessor interface
registerOnChange(fn: any) {
this.propagateChange = fn;
}
//From ControlValueAccessor interface
registerOnTouched(fn: any) {
}
}
下面是自定义输入组件的模板HTML
<div class="fg">
<!--Label text-->
<label [attr.for]="idd">{{text}}<sup *ngIf="!optional">*</sup></label>
<!--Input form control element with on change event listener helpful to propagate changes -->
<input type="{{type}}" #input id="{{idd}}" placeholder="{{pH}}" (blur)="onChange($event, input.value)">
<!--Loop through errors-->
<div style="height:0px;" [@visibilityChanged]="!c.pristine && !c.valid" class="error">
<p *ngFor="let error of errors">{{error}}</p>
</div>
</div>
下面是自定义输入组件,可以在 fromGroup 中使用,也可以单独使用
<inv-input formControlName="title" [c]="newQueryForm.controls.title" [optional]="true" idd="title" placeholder="Type Title to search"
text="Title"></inv-input>
以这种方式,如果您实现自定义表单控件,您可以轻松应用自定义验证器指令并在该表单控件上累积错误以显示您的错误。
可以模仿相同的样式开发自定义select组件、单选按钮组、复选框、文本区域、文件上传等,根据表单控件的行为要求进行微小的更改。
绝对值得深入研究@web-master-now 的回答,但要简单地回答问题,您只需要 ElementRef
将 formControlName
引用到输入中。
所以如果你有一个简单的表格
this.userForm = this.formBuilder.group({
name: [this.user.name, [Validators.required]],
email: [this.user.email, [Validators.required]]
});
那么你的父组件的 html 将是
<form [formGroup]="userForm" no-validate>
<custom-input formControlName="name"
// very useful to pass the actual control item
[control]="userForm.controls.name"
[label]="'Name'">
</custom-input>
<custom-input formControlName="email"
[control]="userForm.controls.email"
[label]="'Email'">
</custom-input>
...
</form>
然后在你的自定义组件中custom-input.ts
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'custom-input',
templateUrl: 'custom-input.html',
})
export class YInputItem {
@Input('label') inputLabel: string;
@Input() control: FormControl;
@ViewChild('input') inputRef: ElementRef;
constructor() {
}
ngAfterViewInit(){
// You should see the actual form control properties being passed in
console.log('control',this.control);
}
}
然后在组件的htmlcustom-input.html
<label>
{{ inputLabel }}
</label>
<input #input/>
绝对值得一试 ControlValueAccessor,但根据您开发控件的方式,您可能只想使用 @Output
来监听更改事件,即如果表单中的不同输入有不同的事件,你可以把逻辑放在父组件里监听。
您可以使用 ion-input-auto-complete 组件获取输入值,根据您的代码使用下面的代码
<form [formGroup]="userForm" no-validate>
<input-auto-complete formControlName="name"
[ctrl]="userForm.controls['name']"
[label]="'Name'">
</input-auto-complete>
</form>
希望这个简单的用例可以帮助别人。
这是一个phone数字屏蔽组件的示例,它允许您传入表单组并在组件内部引用表单控件。
子组件 - phone-input.component.html
在包含 div 中添加对 FormGroup 的引用,并像往常一样传入 formControlName输入。
<div [formGroup]="pFormGroup">
<input [textMask]="phoneMaskingDef" class="form-control" [formControlName]="pControlName" >
</div>
父组件 - form.component.html
引用组件并传入 pFormGroup 和 pControlName 作为属性。
<div class="form-group">
<label>Home</label>
<phone-input [pFormGroup]="myForm" pControlName="homePhone"></phone-input>
</div>
我正在以类似 的方式解决这个问题。但是我没有编写一个完整的自己的 ControlValueAccessor
,而是将所有内容委托给一个内部的 <input>
ControlValueAccessor
。结果是一个更短的代码,我不必自己处理与 <input>
元素的交互。
这是我的代码
@Component({
selector: 'form-field',
template: `
<label>
{{label}}
<input ngDefaultControl type="text" >
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor, AfterViewInit {
@Input() label: String;
@Input() formControlName: String;
@ViewChild(DefaultValueAccessor) valueAccessor: DefaultValueAccessor;
delegatedMethodCalls = new ReplaySubject<(_: ControlValueAccessor) => void>();
ngAfterViewInit(): void {
this.delegatedMethodCalls.subscribe(fn => fn(this.valueAccessor));
}
registerOnChange(fn: (_: any) => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnChange(fn));
}
registerOnTouched(fn: () => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnTouched(fn));
}
setDisabledState(isDisabled: boolean): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.setDisabledState(isDisabled));
}
writeValue(obj: any): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.writeValue(obj));
}
}
它是如何工作的?
通常这不会起作用,因为没有 formControlName
指令的简单 <input>
将不会是 ControlValueAccessor
,由于缺少 formControlName
指令,组件中不允许这样做=23=],正如其他人已经指出的那样。但是,如果我们查看 Angular 的 DefaultValueAccessor
实现代码
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
//...
})
export class DefaultValueAccessor implements ControlValueAccessor {
...我们可以看到还有一个属性选择器ngDefaultControl
。它可用于不同的目的,但它似乎得到官方支持。
一个小缺点是 @ViewChild
带有值访问器的查询结果在调用 ngAfterViewInit
处理程序之前不可用。 (根据您的模板,它会更早可用,但官方不支持。)
这就是为什么我要使用 ReplaySubject
缓冲我们要委托给内部 DefaultValueAccessor
的所有调用。 ReplaySubject
是一个 Observable
,它缓冲所有事件并在订阅时发出它们。正常的 Subject
会在订阅之前将它们丢弃。
我们发出 lambda 表达式,表示可以稍后执行的实际调用。在 ngAfterViewInit
我们订阅我们的 ReplaySubject
并简单地调用接收到的 lambda 函数。
我在这里分享另外两个想法,因为它们对我自己的项目非常重要,我花了一些时间来解决所有问题。我看到很多人有类似的问题和用例,所以我希望这对你有用:
改进思路1:为视图提供FormControl
我在我的项目中用formControl
替换了ngDefaultControl
,所以我们可以将FormControl
实例传递给内部<input>
。这本身并没有用,但是如果您使用其他与 FormControl
交互的指令,例如 Angular Material 的 MatInput
,它就会有用。例如。如果我们将 form-field
模板替换为...
<mat-form-field>
<input [placeholder]="label" [formControl]="formControl>
<mat-error>Error!</mat-error>
</mat-form-field>
...Angular Material 能够自动显示表单控件中设置的错误。
我必须调整组件才能通过表单控制。我从我们的 FormControlName
指令中检索表单控件:
export class FormFieldComponent implements ControlValueAccessor, AfterContentInit {
// ... see above
@ContentChild(FormControlName) private formControlNameRef: FormControlName;
formControl: FormControl;
ngAfterContentInit(): void {
this.formControl = <FormControl>this.formControlNameRef.control;
}
// ... see above
}
您还应该调整您的选择器以要求 formControlName
属性:selector: 'form-field[formControlName]'
.
改进想法 2:委托给更通用的值访问器
我用所有 ControlValueAccessor
实现的查询替换了 DefaultValueAccessor
@ViewChild
查询。这允许除 <input>
之外的其他 HTML 表单控件,例如 <select>
,如果您想让表单控件类型可配置,这将很有用。
@Component({
selector: 'form-field',
template: `
<label [ngSwitch]="controlType">
{{label}}
<input *ngSwitchCase="'text'" ngDefaultControl type="text" #valueAccessor>
<select *ngSwitchCase="'dropdown'" ngModel #valueAccessor>
<ng-content></ng-content>
</select>
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor {
// ... see above
@Input() controlType: String = 'text';
@ViewChild('valueAccessor', {read: NG_VALUE_ACCESSOR}) valueAccessor: ControlValueAccessor;
// ... see above
}
用法示例:
<form [formGroup]="form">
<form-field formControlName="firstName" label="First Name"></form-field>
<form-field formControlName="lastName" label="Last Name" controlType="dropdown">
<option>foo</option>
<option>bar</option>
</form-field>
<p>Hello "{{form.get('firstName').value}} {{form.get('lastName').value}}"</p>
</form>
上面 select
的一个问题是 ngModel
is already deprecated together with reactive forms。不幸的是,对于 Angular 的 <select>
控制值访问器,没有像 ngDefaultControl
这样的东西。因此我建议结合我的第一个改进想法
Angular 8 和 9:
在您的自定义组件中使用 viewProvider。工作示例:
@Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective
}
]
})
现在,当您分配 formControlName 时,您的组件将自身附加到父表单。
<input matInput formControlName="{{name}}">
或
<input matInput [formControlName]='name'>
我想创建一个可以与 FormBuilder 一起使用的自定义输入组件 API。如何在组件中添加 formControlName
?
模板:
<label class="custom-input__label"
*ngIf="label">
{{ label }}
</label>
<input class="custom-input__input"
placeholder="{{ placeholder }}"
name="title" />
<span class="custom-input__message"
*ngIf="message">
{{ message }}
</span>
组件:
import {
Component,
Input,
ViewEncapsulation
} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'custom-input',
host: {
'[class.custom-input]': 'true'
},
templateUrl: 'input.component.html',
styleUrls: ['input.component.css'],
encapsulation: ViewEncapsulation.None,
})
export class InputComponent {
@Input() label: string;
@Input() message: string;
@Input() placeholder: string;
}
用法:
<custom-input label="Title"
formControlName="title" // Pass this to input inside the component>
</custom-input>
这里的主要思想是你必须 link FormControl 到 FormGroup,这可以通过将 FormGroup 传递到每个输入组件来完成...
因此您的输入模板可能如下所示:
<div [formGroup]="form">
<label *ngIf="label">{{ label }}</label>
<input [formControlName]="inputName" />
<span *ngIf="message">{{ message }}</span>
</div>
输入组件的 @Input
将是 form
、label
、inputName
和 message
。
可以这样使用:
<form [FormGroup]="yourFormGroup">
<custom-input
[form]="yourFormGroup"
[inputName]="thisFormControlName"
[message]="yourMessage"
[label]="yourLabel">
</custom-input>
</form>
有关自定义表单输入组件的更多信息,我建议您查看 Angular's Dynamic Forms。
此外,如果您想了解有关如何使 @Input
和 @Output
正常工作的更多信息,请查看 Angular Docs Here
您不应将 formControlName
属性添加到自定义组件模板中的输入字段。
根据最佳实践,您应该在自定义输入元素本身上添加 formControlName
。
在这里,您可以在自定义输入组件中使用的是 controlValueAccessor
界面,只要您的自定义输入模板中的输入字段事件发生更改,您的自定义输入就会更新值或模糊。
它在您的自定义输入的表单控件行为与您为该自定义表单控件提供的 UI 之间提供连接(以更新值或其他需求)。
下面是TypeScript自定义输入组件的代码
import { Component, Input, forwardRef, AfterViewInit, trigger, state, animate, transition, style, HostListener, OnChanges, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl } from '@angular/forms';
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
};
@Component({
selector: 'inv-input',
templateUrl:'./input-text.component.html',
styleUrls: ['./input-text.component.css'],
encapsulation: ViewEncapsulation.None,
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR],
animations:[trigger(
'visibilityChanged',[
state('true',style({'height':'*','padding-top':'4px'})),
state('false',style({height:'0px','padding-top':'0px'})),
transition('*=>*',animate('200ms'))
]
)]
})
export class InputComponent implements ControlValueAccessor, AfterViewInit, OnChanges {
// Input field type eg:text,password
@Input() type = "text";
// ID attribute for the field and for attribute for the label
@Input() idd = "";
// The field name text . used to set placeholder also if no pH (placeholder) input is given
@Input() text = "";
// placeholder input
@Input() pH:string;
//current form control input. helpful in validating and accessing form control
@Input() c:FormControl = new FormControl();
// set true if we need not show the asterisk in red color
@Input() optional : boolean = false;
//@Input() v:boolean = true; // validation input. if false we will not show error message.
// errors for the form control will be stored in this array
errors:Array<any> = ['This field is required'];
// get reference to the input element
@ViewChild('input') inputRef:ElementRef;
constructor() {
}
ngOnChanges(){
}
//Lifecycle hook. angular.io for more info
ngAfterViewInit(){
// set placeholder default value when no input given to pH property
if(this.pH === undefined){
this.pH = "Enter "+this.text;
}
// RESET the custom input form control UI when the form control is RESET
this.c.valueChanges.subscribe(
() => {
// check condition if the form control is RESET
if (this.c.value == "" || this.c.value == null || this.c.value == undefined) {
this.innerValue = "";
this.inputRef.nativeElement.value = "";
}
}
);
}
//The internal data model for form control value access
private innerValue: any = '';
// event fired when input value is changed . later propagated up to the form control using the custom value accessor interface
onChange(e:Event, value:any){
//set changed value
this.innerValue = value;
// propagate value into form control using control value accessor interface
this.propagateChange(this.innerValue);
//reset errors
this.errors = [];
//setting, resetting error messages into an array (to loop) and adding the validation messages to show below the field area
for (var key in this.c.errors) {
if (this.c.errors.hasOwnProperty(key)) {
if(key === "required"){
this.errors.push("This field is required");
}else{
this.errors.push(this.c.errors[key]);
}
}
}
}
//get accessor
get value(): any {
return this.innerValue;
};
//set accessor including call the onchange callback
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
}
}
//propagate changes into the custom form control
propagateChange = (_: any) => { }
//From ControlValueAccessor interface
writeValue(value: any) {
this.innerValue = value;
}
//From ControlValueAccessor interface
registerOnChange(fn: any) {
this.propagateChange = fn;
}
//From ControlValueAccessor interface
registerOnTouched(fn: any) {
}
}
下面是自定义输入组件的模板HTML
<div class="fg">
<!--Label text-->
<label [attr.for]="idd">{{text}}<sup *ngIf="!optional">*</sup></label>
<!--Input form control element with on change event listener helpful to propagate changes -->
<input type="{{type}}" #input id="{{idd}}" placeholder="{{pH}}" (blur)="onChange($event, input.value)">
<!--Loop through errors-->
<div style="height:0px;" [@visibilityChanged]="!c.pristine && !c.valid" class="error">
<p *ngFor="let error of errors">{{error}}</p>
</div>
</div>
下面是自定义输入组件,可以在 fromGroup 中使用,也可以单独使用
<inv-input formControlName="title" [c]="newQueryForm.controls.title" [optional]="true" idd="title" placeholder="Type Title to search"
text="Title"></inv-input>
以这种方式,如果您实现自定义表单控件,您可以轻松应用自定义验证器指令并在该表单控件上累积错误以显示您的错误。
可以模仿相同的样式开发自定义select组件、单选按钮组、复选框、文本区域、文件上传等,根据表单控件的行为要求进行微小的更改。
绝对值得深入研究@web-master-now 的回答,但要简单地回答问题,您只需要 ElementRef
将 formControlName
引用到输入中。
所以如果你有一个简单的表格
this.userForm = this.formBuilder.group({
name: [this.user.name, [Validators.required]],
email: [this.user.email, [Validators.required]]
});
那么你的父组件的 html 将是
<form [formGroup]="userForm" no-validate>
<custom-input formControlName="name"
// very useful to pass the actual control item
[control]="userForm.controls.name"
[label]="'Name'">
</custom-input>
<custom-input formControlName="email"
[control]="userForm.controls.email"
[label]="'Email'">
</custom-input>
...
</form>
然后在你的自定义组件中custom-input.ts
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'custom-input',
templateUrl: 'custom-input.html',
})
export class YInputItem {
@Input('label') inputLabel: string;
@Input() control: FormControl;
@ViewChild('input') inputRef: ElementRef;
constructor() {
}
ngAfterViewInit(){
// You should see the actual form control properties being passed in
console.log('control',this.control);
}
}
然后在组件的htmlcustom-input.html
<label>
{{ inputLabel }}
</label>
<input #input/>
绝对值得一试 ControlValueAccessor,但根据您开发控件的方式,您可能只想使用 @Output
来监听更改事件,即如果表单中的不同输入有不同的事件,你可以把逻辑放在父组件里监听。
您可以使用 ion-input-auto-complete 组件获取输入值,根据您的代码使用下面的代码
<form [formGroup]="userForm" no-validate>
<input-auto-complete formControlName="name"
[ctrl]="userForm.controls['name']"
[label]="'Name'">
</input-auto-complete>
</form>
希望这个简单的用例可以帮助别人。
这是一个phone数字屏蔽组件的示例,它允许您传入表单组并在组件内部引用表单控件。
子组件 - phone-input.component.html
在包含 div 中添加对 FormGroup 的引用,并像往常一样传入 formControlName输入。
<div [formGroup]="pFormGroup">
<input [textMask]="phoneMaskingDef" class="form-control" [formControlName]="pControlName" >
</div>
父组件 - form.component.html
引用组件并传入 pFormGroup 和 pControlName 作为属性。
<div class="form-group">
<label>Home</label>
<phone-input [pFormGroup]="myForm" pControlName="homePhone"></phone-input>
</div>
我正在以类似 ControlValueAccessor
,而是将所有内容委托给一个内部的 <input>
ControlValueAccessor
。结果是一个更短的代码,我不必自己处理与 <input>
元素的交互。
这是我的代码
@Component({
selector: 'form-field',
template: `
<label>
{{label}}
<input ngDefaultControl type="text" >
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor, AfterViewInit {
@Input() label: String;
@Input() formControlName: String;
@ViewChild(DefaultValueAccessor) valueAccessor: DefaultValueAccessor;
delegatedMethodCalls = new ReplaySubject<(_: ControlValueAccessor) => void>();
ngAfterViewInit(): void {
this.delegatedMethodCalls.subscribe(fn => fn(this.valueAccessor));
}
registerOnChange(fn: (_: any) => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnChange(fn));
}
registerOnTouched(fn: () => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnTouched(fn));
}
setDisabledState(isDisabled: boolean): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.setDisabledState(isDisabled));
}
writeValue(obj: any): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.writeValue(obj));
}
}
它是如何工作的?
通常这不会起作用,因为没有 formControlName
指令的简单 <input>
将不会是 ControlValueAccessor
,由于缺少 formControlName
指令,组件中不允许这样做=23=],正如其他人已经指出的那样。但是,如果我们查看 Angular 的 DefaultValueAccessor
实现代码
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
//...
})
export class DefaultValueAccessor implements ControlValueAccessor {
...我们可以看到还有一个属性选择器ngDefaultControl
。它可用于不同的目的,但它似乎得到官方支持。
一个小缺点是 @ViewChild
带有值访问器的查询结果在调用 ngAfterViewInit
处理程序之前不可用。 (根据您的模板,它会更早可用,但官方不支持。)
这就是为什么我要使用 ReplaySubject
缓冲我们要委托给内部 DefaultValueAccessor
的所有调用。 ReplaySubject
是一个 Observable
,它缓冲所有事件并在订阅时发出它们。正常的 Subject
会在订阅之前将它们丢弃。
我们发出 lambda 表达式,表示可以稍后执行的实际调用。在 ngAfterViewInit
我们订阅我们的 ReplaySubject
并简单地调用接收到的 lambda 函数。
我在这里分享另外两个想法,因为它们对我自己的项目非常重要,我花了一些时间来解决所有问题。我看到很多人有类似的问题和用例,所以我希望这对你有用:
改进思路1:为视图提供FormControl
我在我的项目中用formControl
替换了ngDefaultControl
,所以我们可以将FormControl
实例传递给内部<input>
。这本身并没有用,但是如果您使用其他与 FormControl
交互的指令,例如 Angular Material 的 MatInput
,它就会有用。例如。如果我们将 form-field
模板替换为...
<mat-form-field>
<input [placeholder]="label" [formControl]="formControl>
<mat-error>Error!</mat-error>
</mat-form-field>
...Angular Material 能够自动显示表单控件中设置的错误。
我必须调整组件才能通过表单控制。我从我们的 FormControlName
指令中检索表单控件:
export class FormFieldComponent implements ControlValueAccessor, AfterContentInit {
// ... see above
@ContentChild(FormControlName) private formControlNameRef: FormControlName;
formControl: FormControl;
ngAfterContentInit(): void {
this.formControl = <FormControl>this.formControlNameRef.control;
}
// ... see above
}
您还应该调整您的选择器以要求 formControlName
属性:selector: 'form-field[formControlName]'
.
改进想法 2:委托给更通用的值访问器
我用所有 ControlValueAccessor
实现的查询替换了 DefaultValueAccessor
@ViewChild
查询。这允许除 <input>
之外的其他 HTML 表单控件,例如 <select>
,如果您想让表单控件类型可配置,这将很有用。
@Component({
selector: 'form-field',
template: `
<label [ngSwitch]="controlType">
{{label}}
<input *ngSwitchCase="'text'" ngDefaultControl type="text" #valueAccessor>
<select *ngSwitchCase="'dropdown'" ngModel #valueAccessor>
<ng-content></ng-content>
</select>
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor {
// ... see above
@Input() controlType: String = 'text';
@ViewChild('valueAccessor', {read: NG_VALUE_ACCESSOR}) valueAccessor: ControlValueAccessor;
// ... see above
}
用法示例:
<form [formGroup]="form">
<form-field formControlName="firstName" label="First Name"></form-field>
<form-field formControlName="lastName" label="Last Name" controlType="dropdown">
<option>foo</option>
<option>bar</option>
</form-field>
<p>Hello "{{form.get('firstName').value}} {{form.get('lastName').value}}"</p>
</form>
上面 select
的一个问题是 ngModel
is already deprecated together with reactive forms。不幸的是,对于 Angular 的 <select>
控制值访问器,没有像 ngDefaultControl
这样的东西。因此我建议结合我的第一个改进想法
Angular 8 和 9: 在您的自定义组件中使用 viewProvider。工作示例:
@Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective
}
]
})
现在,当您分配 formControlName 时,您的组件将自身附加到父表单。
<input matInput formControlName="{{name}}">
或
<input matInput [formControlName]='name'>