Angular 2 - formControlName внутри компонента

Я хочу создать пользовательский компонент ввода, который я могу использовать с API FormBuilder. Как добавить 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>

Ответ 1

Не следует добавлять атрибут formControlName к полю ввода в шаблоне вашего настраиваемого компонента. Вы должны добавить formControlName в элемент пользовательского ввода в соответствии с наилучшей практикой.

Здесь то, что вы можете использовать в своем компоненте пользовательского ввода, это интерфейс controlValueAccessor, чтобы ваш пользовательский ввод обновлял значение всякий раз, когда в шаблоне вашего пользовательского ввода было изменено или размыто событие ввода в шаблоне вашего пользовательского ввода.

Он обеспечивает соединение (для обновления значений или других потребностей) между поведением управления формой вашего пользовательского ввода и пользовательским интерфейсом, который вы предоставляете для этого пользовательского элемента управления формой.

Ниже приведен код пользовательского входного компонента в 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>

Ниже представлен пользовательский компонент ввода, который можно использовать в группе или отдельно

<inv-input formControlName="title" [c]="newQueryForm.controls.title" [optional]="true" idd="title" placeholder="Type Title to search"
          text="Title"></inv-input>

Таким образом, если вы реализуете свои настраиваемые элементы управления форматом, вы можете легко применить свои настраиваемые директивы валидатора и скопировать ошибки в этом элементе управления для отображения своих ошибок.

Можно имитировать один и тот же стиль для разработки настраиваемого компонента выбора, группы переключателей, checkbox, textarea, fileupload и т.д. вышеописанным способом с незначительными изменениями в соответствии с требованиями поведения управления формой.

Ответ 2

Основная идея здесь заключается в том, что вам нужно связать 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 Динамические формы. Также, если вы хотите получить дополнительную информацию о том, как получить работу @Input и @Output, просмотрите Angular Docs Here

Ответ 3

Определенно стоит углубиться в ответ @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 для прослушивания событий изменения, т.е. если разные входы в форме имеют разные события Вы можете просто поместить логику в родительский компонент и слушать.

Ответ 4

Надеюсь, этот простой вариант использования может кому-то помочь.

Это пример компонента маскирования телефонных номеров, который позволяет вам передавать группу форм и ссылаться на элемент управления формы внутри компонента.

Дочерний компонент - phone-input.component.html

Добавьте ссылку на FormGroup в содержащий div и передайте в 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>

Ответ 5

Я решаю эту проблему аналогично 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));
  }
}

Как это работает?

Как правило, это не сработает, поскольку simpel <input> не будет ControlValueAccessor без formControlName -directive, что недопустимо в компоненте из-за отсутствия [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, используя ReplaySubject. ReplaySubject - это Observable, который буферизует все события и генерирует их при подписке. Обычный Subject выбросил бы их до подписки.

Мы испускаем лямбда-выражения, представляющие фактический вызов, который может быть выполнен позже. На ngAfterViewInit мы подписываемся на наш ReplaySubject и просто вызываем полученные лямбда-функции.

Здесь я делюсь двумя другими идеями, так как они очень важны для моих собственных проектов, и мне потребовалось время, чтобы все уладить. Я вижу много людей, имеющих схожие проблемы и варианты использования, поэтому я надеюсь, что это будет полезно для вас:

Идея улучшения 1: предоставить FormControl для вида

Я заменил ngDefaultControl на formControl в моем проекте, чтобы мы могли передать экземпляр 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: делегировать доступ к более общему значению

Я заменил запрос DefaultValueAccessor @ViewChild на запрос для всех реализаций ControlValueAccessor. Это позволяет использовать другие элементы управления HTML-формами, кроме <input>, например <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 уже устарела вместе с реактивными формами. К сожалению, нет ничего похожего на ngDefaultControl для акселератора Angular <select>. Поэтому я предлагаю совместить это с моей первой идеей улучшения.

Ответ 6

Вы можете получить входное значение с помощью компонента ion-input-auto-complete, как показано в коде ниже

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

Ответ 7

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