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>
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.
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.
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>
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>
.
@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));
}
}
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:
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 FormControl
s, 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]'
.
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 ngModel
est 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.
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>