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