Angular 2 - 组件内部的formControlName

时间:2016-09-23 12:59:33

标签: angular angular2-forms

我想创建一个可以与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>

7 个答案:

答案 0 :(得分:27)

您不应将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>

以这种方式实现自定义表单控件时,您可以轻松应用自定义验证程序指令,并在该表单控件上累积错误以显示错误。

可以模仿相同的样式,以上述方式开发自定义选择组件,单选按钮组,复选框,文本区域,文件上载等,并根据表单控件的行为要求进行微小更改。

答案 1 :(得分:9)

这里的主要想法是你必须将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

答案 2 :(得分:4)

角度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'>

答案 3 :(得分:1)

绝对值得深入了解@ 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来监听更改事件,即表单中的不同输入是否有不同的事件,你可以把逻辑放在父组件中然后听。

答案 4 :(得分:1)

我正在以类似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));
  }
}

它如何工作?

通常这是行不通的,因为没有<input>伪指令的Simpel ControlValueAccessor不会是formControlName,由于缺少{{1 }},正如其他人已经指出的那样。但是,如果我们看一下[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缓冲所有要委派给内部DefaultValueAccessor的呼叫的原因。 ReplaySubjectReplaySubject,它缓冲所有事件并在订阅时发出它们。普通的Observable会把它们扔掉直到订阅。

我们发出表示可以在以后执行的实际调用的lambda表达式。在Subject上,我们订阅了ngAfterViewInit,只需调用接收到的lambda函数即可。

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

改进思路:为视图提供ReplaySubject

我在项目中将FormControl替换为ngDefaultControl,因此我们可以将formControl实例传递给内部FormControl。它本身并没有用,但是如果您正在使用与<input>交互的其他指令,例如Angular Material的MatInput,就没有用。例如。如果我们将FormControl模板替换为...

form-field

... Angular Material能够自动显示在表单控件中设置的错误。

我必须调整组件才能通过表单控件。我从<mat-form-field> <input [placeholder]="label" [formControl]="formControl> <mat-error>Error!</mat-error> </mat-form-field> 指令中检索表单控件:

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

改进思路2:委托给更通用的值访问器

我用对所有selector: 'form-field[formControlName]'实现的查询代替了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 is already deprecated together with reactive forms。不幸的是,Angular的ngModel控制值访问器没有像ngDefaultControl那样的东西。因此,我建议将此与我的第一个改进想法结合起来。

答案 5 :(得分:0)

您可以使用ion-input-auto-complete组件获取输入值,根据代码使用代码

<form [formGroup]="userForm" no-validate>
   <input-auto-complete formControlName="name"
                 [ctrl]="userForm.controls['name']"
                 [label]="'Name'">
   </input-auto-complete>
</form>

答案 6 :(得分:0)

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

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

子组件 - phone-input.component.html

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

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

父组件 - form.component.html

引用该组件并将 pFormGroup pControlName 作为atttributes传递。

<div class="form-group">
     <label>Home</label>
     <phone-input [pFormGroup]="myForm" pControlName="homePhone"></phone-input>
</div>