Angulaire 2 - formControlName à l'intérieur de la composante

je veux créer un composant d'entrée personnalisé que je peux utiliser avec L'API FormBuilder. Comment puis-je 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;
}

Utilisation:

<custom-input label="Title" 
           formControlName="title" // Pass this to input inside the component>
</custom-input>
15
demandé sur BenWurth 2016-09-23 15:59:33

6 réponses

vous ne devriez pas ajouter formControlName attribuez au champ d'entrée dans le modèle de votre composant personnalisé. Vous devez ajouter le formControlName sur l'élément custom-input lui-même selon la meilleure pratique.

Ici ce que vous pouvez utiliser dans votre personnalisé-entrée composante est l' controlValueAccessor interface pour faire votre custom-input avoir la valeur mise à jour chaque fois qu'il y a un événement de champ d'entrée dans le modèle de votre input personnalisé changé ou flou.

Il fournit une connexion (pour mettre à jour les valeurs ou d'autres besoins) entre le comportement de contrôle de forme de votre entrée personnalisée et L'UI que vous fournissez pour ce contrôle de forme personnalisée.

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

    }
}

ci-dessous se trouve 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 est un composant d'entrée personnalisé qui peut être utilisé dans un individuellement

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

de cette façon, si vous mettez en œuvre vos contrôles de formulaires personnalisés, vous pouvez appliquer vos directives de validateur personnalisé facilement et accumuler les erreurs sur ce contrôle de formulaires pour afficher vos erreurs.

on peut imiter le même style pour développer custom select component, radio button group, checkbox, textarea, fileupload etc de la façon ci-dessus avec des changements mineurs selon ce que le comportement du contrôle de forme exige.

18
répondu web-master-now 2017-09-07 05:31:27

l'idée principale ici est que vous devez relier le FormControl au FormGroup, cela peut être fait en passant le FormGroup à chaque composant d'entrée...

alors votre modèle de saisie pourrait ressembler à quelque chose comme ceci:

<div [formGroup]="form">
    <label *ngIf="label">{{ label }}</label>
    <input [formControlName]="inputName" />
    <span *ngIf="message">{{ message }}</span>
</div>

@Input's pour le composant d'entrée sera form,label,inputName et message.

Il serait utilisé comme ceci:

<form [FormGroup]="yourFormGroup">
    <custom-input
        [form]="yourFormGroup"
        [inputName]="thisFormControlName"
        [message]="yourMessage"
        [label]="yourLabel">
    </custom-input>
</form>

pour plus d'information sur les composantes d'entrée de formulaire personnalisé, je voudrais recommande de prendre un coup d'oeil par le biais de Angulaire de la Dynamique des Formes. Aussi, si vous souhaitez plus d'informations sur la façon d'obtenir le @Input et @Output travailler jeter un coup d'oeil à travers le Angulaire Docs Ici

7
répondu P. Moloney 2016-09-26 07:24:55

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

Donc si vous avez un simple formulaire

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

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

Puis dans votre composant personnalisé personnalisé-entrée.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 puis dans le html du composant personnalisé-entrée.html

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

certainement intéressant de vérifier le ControlValueAccessor, mais en fonction de comment vous le développement du contrôle, vous pouvez utiliser @Output to listen to change events, I. e si différentes entrées dans la forme ont des événements différents, vous pouvez simplement mettre la logique dans le composant parent et écouter.

1
répondu BenWurth 2018-04-03 19:40:58

Vous pouvez obtenir la valeur d'entrée en utilisant le ion-entrée-de l'auto-complétion composant, comme par votre code utilise 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
répondu Zameel 2018-06-01 09:11:46

espérons que ce simple cas d'utilisation peut aider quelqu'un.

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

composant enfant-entrée par téléphone.composant.html

ajouter une référence au FormGroup dans le div contenant et passer dans le formControlName comme vous le feriez normalement sur l'entrée.

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

Composant Parent - forme.composant.html

référez au composant et passez en pFormGroup et pControlName propriétés.

<div class="form-group">
     <label>Home</label>
     <phone-input [pFormGroup]="myForm" pControlName="homePhone"></phone-input>
</div>
0
répondu shadowfox476 2018-06-18 19:25:35

Je résous cela d'une manière similaire comme web-master-maintenant. Mais au lieu d'écrire un complet ControlValueAccessor je suis déléguer tout à l'intérieur <input>ControlValueAccessor. Le résultat est un code beaucoup plus court et je n'ai pas à gérer l'interaction avec le <input> élément sur mon propre.

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 Général, cela ne fonctionne pas, comme avec <input> ne ControlValueAccessor sans formControlName - directive, ce qui n'est pas autorisé dans le composant en raison de l'absence de [formGroup] comme d'autres l'ont signalé. Cependant, si nous regardons le code D'Angular pour le DefaultValueAccessor mise en oeuvre

@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 y a un autre attribut sélecteur ngDefaultControl. Il est disponible dans un but différent, mais il semble être soutenu officiellement.

un petit inconvénient est que le @ViewChild résultat de la requête avec la valeur accessor ne sera pas disponible avant le ngAfterViewInit gestionnaire est appelé. (Il sera disponible plus tôt selon votre modèle, mais ce n'est pas pris en charge officiellement .)

C'est pourquoi je me suis mise en mémoire tampon tous les appels que nous voulons délégué à l'intérieur de notre DefaultValueAccessor à l'aide d'un ReplaySubject. ReplaySubject est un Observable, qui tamponne tous les événements et les émet sur abonnement. Normale Subject les jeter jusqu'à l'abonnement.

nous émettons des expressions lambda représentant l'appel réel qui peut être exécuté plus tard. ngAfterViewInit nous vous abonner à notre ReplaySubject et appelez 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 similaires et des cas d'utilisation, donc j'espère que cela est utile pour vous:

Idée d'amélioration 1: Fournir l' FormControl pour afficher

j'ai remplacé les ngDefaultControl par formControl dans mon projet, donc nous pouvons passer l' FormControl instance de l'intérieur <input>. Ce n'est pas utile en soi, mais il est si vous utilisez d'autres directives qui interagissent avec FormControl s, comme les matériaux angulaires MatInput. E. g. si nous remplaçons notre form-field modèle par...

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

...Le matériel angulaire est capable d'afficher automatiquement des erreurs définies dans le contrôle de forme.

je dois ajuster le composant afin de passer le contrôle de formulaire. Je récupère le contrôle de forme de notre FormControlName directive:

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 devriez également ajuster votre sélecteur pour avoir besoin du formControlName l'attribut: selector: 'form-field[formControlName]'.

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

j'ai remplacé les DefaultValueAccessor@ViewChild requête en une requête pour tous les ControlValueAccessor implémentations. Cela permet d'autres contrôles de forme HTML que <input><select> et est utile si vous voulez rendre votre type de contrôle de forme 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
}

Utilisation exemple:

<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 le select dessus ngModel est déjà déprécié avec des formes réactives. Malheureusement, il n'y a rien comme ngDefaultControl Angulaire <select> valeur de contrôle de l'accesseur. Je suggère donc de combiner ceci avec ma première idée d'amélioration.

0
répondu fishbone 2018-08-17 07:21:52