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 将是formlabelinputNamemessage

它会这样使用:

<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,而是将所有内容委托给内部&lt;input&gt;ControlValueAccessor。结果是一个更短的代码,我不必自己处理与&lt;input&gt; 元素的交互。

这是我的代码

@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 的简单&lt;input&gt; 不会是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 的所有调用。 ReplaySubjectObservable,它缓冲所有事件并在订阅时发出它们。普通的Subject 会在订阅之前将它们丢弃。

我们发出 lambda 表达式,表示可以稍后执行的实际调用。在 ngAfterViewInit 上,我们订阅了 ReplaySubject 并简单地调用接收到的 lambda 函数。

我在这里分享另外两个想法,因为它们对我自己的项目非常重要,而且我花了一些时间来解决所有问题。我看到很多人有类似的问题和用例,所以我希望这对你有用:

改进思路1:为视图提供FormControl

我在项目中将ngDefaultControl 替换为formControl,因此我们可以将FormControl 实例传递给内部&lt;input&gt;。这本身并没有用,但是如果您使用与FormControls 交互的其他指令,例如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 实现的查询。这允许除 &lt;input&gt; 之外的其他 HTML 表单控件,例如 &lt;select&gt;,如果您想让表单控件类型可配置,这很有用。

@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 的 &lt;select&gt; 控制值访问器,没有像 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 的答案,但要简单地回答问题,您只需要ElementRefformControlName 引用到输入。

所以如果你有一个简单的表格

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() 来获取值,这里也是传递表单控件及其名称,如果您 p​​ssing 表单控件因此无需传递控件名称,则可以直接将其与模板输入绑定,比如:【参考方案6】:

希望这个简单的用例可以对某人有所帮助。

这是一个电话号码屏蔽组件的示例,它允许您传入表单组并引用组件内的表单控件。

子组件 - phone-input.component.html

在包含的 div 中添加对 FormGroup 的引用,并像通常在输入中那样传入 formControlName

<div [formGroup]="pFormGroup">
     <input [textMask]="phoneMaskingDef" class="form-control" [formControlName]="pControlName" >
</div>

父组件 - form.component.html

引用组件并传入 pFormGrouppControlName 作为属性。

<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) 视频自动播放

为啥我无法访问 Angular 组件的传递函数内的类属性?

如何在 Angular 中显示具有自己路由的父组件内的特定子组件?

Angular:如何从服务内的事件中获取数据到我的组件?

在 Angular 9 中动态加载 *ngFor 内的组件

Angular 2:包含子组件的表单