web-dev-qa-db-fra.com

Angular 2 - formControlName à l'intérieur du composant

Je souhaite créer un composant d'entrée personnalisé que je peux utiliser avec l'API FormBuilder. Comment ajouter formControlName à l'intérieur d'un composant?

Modèle: 

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

Composant:

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;
}

Usage:

<custom-input label="Title" 
           formControlName="title" // Pass this to input inside the component>
</custom-input>
17
Rit_XPD

Vous ne devez pas ajouter l'attribut formControlName au champ de saisie du modèle de votre composant personnalisé. Vous devriez ajouter la formControlName à l’élément d’entrée personnalisé, conformément à la meilleure pratique.

Ici, ce que vous pouvez utiliser dans votre composant d’entrée personnalisée est l’interface controlValueAccessor pour que la valeur d’entrée personnalisée soit mise à jour chaque fois qu’un événement de champ d’entrée dans le modèle de votre entrée personnalisée est modifié ou flou.

Il fournit une connexion (pour mettre à jour des valeurs ou d'autres besoins) entre le comportement du contrôle de formulaire de votre entrée personnalisée et l'interface utilisateur que vous fournissez pour ce contrôle de formulaire personnalisé.

Vous trouverez ci-dessous le code d'un composant d'entrée personnalisé dans 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) {

    }
}

Vous trouverez ci-dessous le modèle HTML pour le composant d'entrée personnalisé.

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

Ci-dessous, un composant d’entrée personnalisé pouvant être utilisé dans un groupe fromGroup ou individuellement.

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

De cette manière, si vous implémentez vos contrôles de formulaire personnalisés, vous pouvez facilement appliquer vos directives de validation personnalisées et accumuler les erreurs sur ce contrôle de formulaire pour afficher vos erreurs.

On peut imiter le même style pour développer un composant de sélection personnalisé, un groupe de boutons radio, une case à cocher, une zone de texte, un téléchargement de fichier, etc. de la manière décrite ci-dessus avec des modifications mineures selon les exigences du comportement du contrôle de formulaire.

22
web-master-now

Cela vaut certainement la peine de plonger plus profondément dans la réponse de @ web-master-now, mais simplement pour répondre à la question, vous avez simplement besoin de la variable ElementRef pour référencer la variable formControlName à l'entrée.

Donc si vous avez un formulaire simple

this.userForm = this.formBuilder.group({
  name: [this.user.name, [Validators.required]],
  email: [this.user.email, [Validators.required]]
});

Ensuite, le code HTML de votre composant parent serait 

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

Ensuite, dans votre composant personnalisé 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);
   }
}

Et ensuite dans le code HTML du composant custom-input.html

<label>
    {{ inputLabel }}
</label>
<input #input/>

Cela vaut vraiment la peine de consulter le fichier ControlValueAccessor , mais en fonction de la façon dont vous développez le contrôle, vous pouvez utiliser simplement @Output pour écouter les événements de modification, c'est-à-dire que si différentes entrées du formulaire ont des événements différents, vous pouvez simplement mettre la logique dans le composant parent et écoutez.

1
user1752532

Vous pouvez obtenir la valeur d'entrée à l'aide du composant ion-input-auto-complete , conformément à votre code, utilisez le code ci-dessous.

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

Je résous ceci de la même manière que web-master-now . Mais au lieu d’écrire une propre variable ControlValueAccessor, je délègue tout à un <input>ControlValueAccessor intérieur. Le résultat est un code beaucoup plus court et je n'ai pas à gérer seul l'interaction avec l'élément <input>.

Voici mon code

@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));
  }
}

Comment ça marche?

En règle générale, cela ne fonctionnera pas, car un <input> simpel ne sera pas une directive ControlValueAccessor sans la directive formControlName-, qui n'est pas autorisée dans le composant en raison d'un [formGroup] manquant, comme d'autres l'ont déjà souligné. Cependant, si nous regardons le code d'Angular pour l'implémentation 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 {

... nous pouvons voir qu'il existe un autre sélecteur d'attributs ngDefaultControl. Il est disponible à des fins différentes, mais il semble être pris en charge officiellement.

Un petit inconvénient est que le résultat de la requête @ViewChild avec l'accesseur de valeur sera disponible pas avant l'appel du gestionnaire ngAfterViewInit. (Il sera disponible plus tôt en fonction de votre modèle, mais ce n'est pas officiellement supporté.)

C'est pourquoi je mets en mémoire tampon tous les appels que nous souhaitons déléguer à notre DefaultValueAccessor intérieure en utilisant un ReplaySubject. Une ReplaySubject est une Observable qui met en mémoire tampon tous les événements et les émet lors de la souscription. Une Subject normale les jetterait jusqu'à la souscription.

Nous émettons des expressions lambda représentant l'appel effectif pouvant être exécuté ultérieurement. Sur ngAfterViewInit nous nous abonnons à notre ReplaySubject et appelons simplement les fonctions lambda reçues.

Je partage deux autres idées ici, car elles sont très importantes pour mes propres projets et il m'a fallu un certain temps pour tout régler. Je vois beaucoup de personnes ayant des problèmes et des cas d'utilisation similaires, j'espère que cela vous sera utile:

Idée d'amélioration 1: Fournissez la FormControl pour la vue.

J'ai remplacé ngDefaultControl par formControl dans mon projet afin que nous puissions passer l'instance FormControl au <input> intérieur. Cela n'est pas utile en soi, mais si vous utilisez d'autres directives qui interagissent avec FormControls, telles que MatInput de Angular Material. Par exemple. si nous remplaçons notre template form-field par ...

<mat-form-field>
    <input [placeholder]="label" [formControl]="formControl>
    <mat-error>Error!</mat-error>
</mat-form-field> 

... Angular Material est capable d'afficher automatiquement les erreurs définies dans le contrôle de formulaire.

Je dois ajuster le composant afin de passer le contrôle de formulaire. Je récupère le contrôle de formulaire à partir de notre directive 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
}

Vous devez également ajuster votre sélecteur pour exiger l'attribut formControlName: selector: 'form-field[formControlName]'.

Idée d'amélioration 2: déléguer à un accesseur de valeur plus générique

J'ai remplacé la requête DefaultValueAccessor@ViewChild par une requête pour toutes les implémentations ControlValueAccessor. Cela autorise d'autres contrôles de formulaire HTML que <input> comme <select> et est utile si vous souhaitez que votre type de contrôle de formulaire soit configurable.

@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
}

Exemple d'utilisation:

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

Un problème avec la variable select ci-dessus est que ngModelest déjà déconseillé avec les formes réactives . Malheureusement, il n'y a rien de tel que ngDefaultControl pour l'accesseur de valeur de contrôle <select> d'Angular. Par conséquent, je suggère de combiner cela avec ma première idée d'amélioration.

0
fishbone

Espérons que ce cas d'utilisation simple puisse aider quelqu'un. 

Voici un exemple de composant de masquage de numéro de téléphone qui vous permet de passer dans le groupe de formulaires et de référencer le contrôle de formulaire à l'intérieur du composant. 

Composant enfant - phone-input.component.html

Ajoutez une référence au FormGroup dans le div qui le contient et transmettez le formControlName comme vous le feriez normalement lors de la saisie. 

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

Composant parent - form.component.html

Référencez le composant et transmettez pFormGroup et pControlName comme attributs.

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