Angular 2 - 组件内的 formControlName
Posted
技术标签:
【中文标题】Angular 2 - 组件内的 formControlName【英文标题】:Angular 2 - formControlName inside component 【发布时间】:2017-02-01 08:28:21 【问题描述】:我想创建一个可以与 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>
【问题讨论】:
我正在尝试做类似的事情; @p-moloney 给出的答案是否提供了您需要的所有信息?如果是,请务必将其标记为已接受的答案,谢谢! 我刚刚做了类似的事情。我没有尝试在formControlName
中传递名称,而是直接将带有formControl="formControlObject"
的FormControl
对象传递给自定义输入。 (@Input formControlObject : FormControl
)。
我推荐文章coryrylan.com/blog/… 很好地解释了这一点。
【参考方案1】:
您不应将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>
以这种方式,如果您实现自定义表单控件,则可以轻松应用自定义验证器指令并在该表单控件上累积错误以显示错误。
可以模仿相同的样式,按照表单控件的行为要求,按照上述方式开发自定义选择组件、单选按钮组、复选框、文本区域、文件上传等。
【讨论】:
太棒了!顺便说一句,我觉得奇怪的是,在 ControlValueAccessor 接口上有一个名为 registerOnTouched 的方法用于触摸事件,这是为什么呢? registerOnTouched 函数接受一个回调函数,当您想将控件设置为触摸时可以调用该回调函数。然后由 Angular 2 通过将正确的触摸状态和类添加到 DOM 中的实际元素标签来管理。参考:blog.thoughtram.io/angular/2016/07/27/… 你的回答启发了我一个类似的解决方案。但我委托而不是自己实现它:***.com/a/51890273/395879 我必须将它与blog.angularindepth.com/… 结合使用您需要将您的值访问器注册为提供者。我在 app.module 中找到了另一篇说要这样做的帖子,但由于某种原因我无法让它工作。我将它放入组件声明本身的那一刻,一切正常:) 你救了我的命!!谢谢!!【参考方案2】: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'>
【讨论】:
这看起来是一个有趣的解决方案。有没有人有更多这方面的资源? 找到这个*** answer,值得一试 你能不能完成这个,如何输入和使用这个属性?【参考方案3】:这里的主要思想是您必须将 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',因为它不是 'input' 的已知属性” 创建Plunker 复制此问题,我会尽我所能帮助您解决问题 这里的问题是每个输入都有一个不理想的表单包装器 这个答案有点过时了,Angular 现在有FormGroupName
,其工作方式类似于FormControlName
,即不是包装每个输入,而是简单地添加一个指向FormGroup
in theroy 的指针【参考方案4】:
我正在以类似web-master-now 的方式解决这个问题。但是,我没有编写完整的自己的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
-directive 的简单<input>
不会是ControlValueAccessor
,由于缺少[formGroup]
,组件中不允许这样做,正如其他人已经指出的那样.但是,如果我们查看 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
我在项目中将ngDefaultControl
替换为formControl
,因此我们可以将FormControl
实例传递给内部<input>
。这本身并没有用,但是如果您使用与FormControl
s 交互的其他指令,例如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:委托给更通用的值访问器
我将DefaultValueAccessor
@ViewChild
查询替换为对所有ControlValueAccessor
实现的查询。这允许除 <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
这样的东西。因此我建议将此与我的第一个改进想法结合起来。
【讨论】:
只有当我们将 formGroup 作为@P 传递时,您的代码才有效。莫洛尼说。 ngDefaultControl 对我没有影响,选择器仍然不起作用并要求 formGroup 父级。 对于那些需要导入的人: import Component, Input, forwardRef, AfterViewInit, HostListener, OnChanges, ViewEncapsulation, ViewChild, ElementRef from '@angular/core';从'@angular/forms'导入 NG_VALUE_ACCESSOR,FormControlName, ControlValueAccessor, FormControl, DefaultValueAccessor ;从 'rxjs/ReplaySubject' 导入 ReplaySubject 从 '@angular/core' 导入 AfterContentInit, ContentChild, Directive; @bormat 我不需要通过 formGroup。这里的代码适用于我的应用程序,非常有用 你能提供一个可执行的例子,这样我会发现我的代码的不同之处吗? 谢谢,所以是因为我使用了有角度的材料,所以我得到了这个错误,stackblitz.com/edit/…,我不知道为什么我以为你也在使用有角度的材料。所以我不能使用你的有效方法。在您的代码中,有一个 DelegatingValueAccessor 类,但它似乎没有被使用。【参考方案5】:绝对值得深入研究@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);
然后在组件的html中custom-input.html
<label>
inputLabel
</label>
<input #input/>
绝对值得一试ControlValueAccessor,但根据您开发控件的方式,您可能只想使用@Output
来监听更改事件,即如果表单中的不同输入有不同的事件,您可以把逻辑放到父组件里听。
【讨论】:
控件是如何与输入元素关联的? 有了这个解决方案,我得到了ERROR Error: No value accessor for form control with name: 'name'
@Tabares 应该使用这个符号 userForm.controls['name']
和 userForm.controls['email']
你不需要使用:name:[this.user.name,[Validators.required]],你应该使用name:[“”,[Validators.required]]然后你可以vhange通过调用 formGroup 或 formControl 的 setValue() 或 patchValue() 来获取值,这里也是传递表单控件及其名称,如果您 pssing 表单控件因此无需传递控件名称,则可以直接将其与模板输入绑定,比如:【参考方案6】:
希望这个简单的用例可以对某人有所帮助。
这是一个电话号码屏蔽组件的示例,它允许您传入表单组并引用组件内的表单控件。
子组件 - 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>
【讨论】:
【参考方案7】:您可以使用ion-input-auto-complete 组件获取输入值,根据您的代码使用下面的代码
<form [formGroup]="userForm" no-validate>
<input-auto-complete formControlName="name"
[ctrl]="userForm.controls['name']"
[label]="'Name'">
</input-auto-complete>
</form>
【讨论】:
以上是关于Angular 2 - 组件内的 formControlName的主要内容,如果未能解决你的问题,请参考以下文章
Angular 2+ 组件内的 Chrome (Android) 视频自动播放