Angular 2 entrée de formulaire personnalisé
Comment puis-je créer un composant personnalisé qui fonctionnerait exactement comme la balise native <input>
? Je veux que mon contrôle de forme personnalisé soit capable de supporter ngControl, ngForm, [(ngModel)].
comme je le comprends, j'ai besoin d'implémenter certaines interfaces pour faire fonctionner mon propre contrôle de forme comme natif.
aussi, il semble que la directive ngForm ne se lie que pour la balise <input>
, est-ce exact? Comment puis-je gérer cela?
laissez-moi vous expliquer pourquoi j'en ai besoin. Je veux enrouler plusieurs éléments d'entrée pour les rendre capables de travailler ensemble comme une seule entrée. Est-il d'autres façon de traiter avec qui? Une fois de plus: je veux faire ce contrôle comme un natif. Validation, ngForm, ngModel bidirectionnel et autre.
ps: J'utilise le Typographie.
7 réponses
en fait, il y a deux choses à mettre en œuvre:
- Un composant qui fournit la logique de votre composant formulaire. Il ne s'agit pas d'une entrée puisqu'il sera fourni par
ngModel
lui-même - personnalisé
ControlValueAccessor
qui permettra de mettre en œuvre le pont entre ce composant etngModel
/ngControl
prenons un échantillon. Je veux implémenter un composant qui gère une liste de balises pour entreprise. Le composant permet d'ajouter et de supprimer des balises. Je veux ajouter une validation pour s'assurer que la liste des balises n'est pas vide. Je vais le définir dans mon composant comme décrit ci-dessous:
(...)
import {TagsComponent} from './app.tags.ngform';
import {TagsValueAccessor} from './app.tags.ngform.accessor';
function notEmpty(control) {
if(control.value == null || control.value.length===0) {
return {
notEmpty: true
}
}
return null;
}
@Component({
selector: 'company-details',
directives: [ FormFieldComponent, TagsComponent, TagsValueAccessor ],
template: `
<form [ngFormModel]="companyForm">
Name: <input [(ngModel)]="company.name"
[ngFormControl]="companyForm.controls.name"/>
Tags: <tags [(ngModel)]="company.tags"
[ngFormControl]="companyForm.controls.tags"></tags>
</form>
`
})
export class DetailsComponent implements OnInit {
constructor(_builder:FormBuilder) {
this.company = new Company('companyid',
'some name', [ 'tag1', 'tag2' ]);
this.companyForm = _builder.group({
name: ['', Validators.required],
tags: ['', notEmpty]
});
}
}
le composant TagsComponent
définit la logique pour ajouter et supprimer des éléments dans la liste tags
.
@Component({
selector: 'tags',
template: `
<div *ngIf="tags">
<span *ngFor="#tag of tags" style="font-size:14px"
class="label label-default" (click)="removeTag(tag)">
{{label}} <span class="glyphicon glyphicon-remove"
aria- hidden="true"></span>
</span>
<span> | </span>
<span style="display:inline-block;">
<input [(ngModel)]="tagToAdd"
style="width: 50px; font-size: 14px;" class="custom"/>
<em class="glyphicon glyphicon-ok" aria-hidden="true"
(click)="addTag(tagToAdd)"></em>
</span>
</div>
`
})
export class TagsComponent {
@Output()
tagsChange: EventEmitter;
constructor() {
this.tagsChange = new EventEmitter();
}
setValue(value) {
this.tags = value;
}
removeLabel(tag:string) {
var index = this.tags.indexOf(tag, 0);
if (index != undefined) {
this.tags.splice(index, 1);
this.tagsChange.emit(this.tags);
}
}
addLabel(label:string) {
this.tags.push(this.tagToAdd);
this.tagsChange.emit(this.tags);
this.tagToAdd = '';
}
}
comme vous pouvez le voir, il n'y a pas d'entrée dans ce composant mais une setValue
(le nom n'est pas important ici). Nous l'utiliserons plus tard pour fournir la valeur de ngModel
au composant. Ce component définit un événement à notifier lorsque l'état du component (la liste des tags) est mis à jour.
implémentons maintenant le lien entre ce composant et ngModel
/ ngControl
. Cela correspond à une directive qui implémente l'interface ControlValueAccessor
. Un fournisseur doit être défini pour cette valeur accessor par rapport au token NG_VALUE_ACCESSOR
(n'oubliez pas d'utiliser forwardRef
puisque la directive est défini après).
la directive attachera un écouteur d'événement à l'événement tagsChange
de l'hôte (c'est-à-dire le composant sur lequel la directive est attachée, c'est-à-dire le TagsComponent
). Le onChange
méthode sera appelée lorsque l'événement se produit. Cette méthode correspond à celle enregistrée par Angular2. De cette façon, il sera au courant des changements et des mises à jour en conséquence le contrôle de formulaire associé.
le writeValue
est appelé lorsque la valeur liée le ngForm
est mis à jour. Après avoir injecté le composant attaché sur (i.e. TagsComponent), on pourra l'appeler pour passer cette valeur (voir la méthode précédente setValue
).
n'oubliez pas de fournir le CUSTOM_VALUE_ACCESSOR
dans les reliures de la directive.
voici le code complet de la coutume ControlValueAccessor
:
import {TagsComponent} from './app.tags.ngform';
const CUSTOM_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => TagsValueAccessor), multi: true}));
@Directive({
selector: 'tags',
host: {'(tagsChange)': 'onChange($event)'},
providers: [CUSTOM_VALUE_ACCESSOR]
})
export class TagsValueAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private host: TagsComponent) { }
writeValue(value: any): void {
this.host.setValue(value);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
de cette façon quand je retire tous les tags
de la compagnie, le L'attribut valid
de la commande companyForm.controls.tags
devient automatiquement false
.
voir cet article (section "composant compatible NgModel") pour plus de détails:
Je ne comprends pas pourquoi chaque exemple que je trouve sur internet doit être si compliqué. Pour expliquer un nouveau concept, je pense qu'il est toujours préférable d'avoir l'exemple le plus simple possible. Je l'ai un peu distillé:
HTML pour la forme externe en utilisant le composant implémentant ngModel:
EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>
composant autonome (pas de classe "accessor" séparée - peut-être que je manque le point):
import {Component, Provider, forwardRef, Input} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR, CORE_DIRECTIVES} from "@angular/common";
const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => InputField),
multi: true
});
@Component({
selector : 'inputfield',
template: `<input [(ngModel)]="value">`,
directives: [CORE_DIRECTIVES],
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class InputField implements ControlValueAccessor {
private _value: any = '';
get value(): any { return this._value; };
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
this.onChange(value);
}
onChange = (_) => {};
onTouched = () => {};
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
en fait, je viens d'abstraire toutes ces choses à une classe abstraite que je développe maintenant avec chaque composant que j'ai besoin d'utiliser ngModel. Pour moi, c'est une tonne de surcharge et de code réutilisable qui je peux faire sans.
Edit: Ici, c'est:
import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export abstract class AbstractValueAccessor implements ControlValueAccessor {
_value: any = '';
get value(): any { return this._value; };
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
// warning: comment below if only want to emit on user intervention
this.onChange(value);
}
onChange = (_) => {};
onTouched = () => {};
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
export function MakeProvider(type : any){
return {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => type),
multi: true
};
}
voici un composant qui l'utilise: (TS):
import {Component, Input} from "@angular/core";
import {CORE_DIRECTIVES} from "@angular/common";
import {AbstractValueAccessor, MakeProvider} from "../abstractValueAcessor";
@Component({
selector : 'inputfield',
template: require('./genericinput.component.ng2.html'),
directives: [CORE_DIRECTIVES],
providers: [MakeProvider(InputField)]
})
export class InputField extends AbstractValueAccessor {
@Input('displaytext') displaytext: string;
@Input('placeholder') placeholder: string;
}
HTML:
<div class="form-group">
<label class="control-label" >{{displaytext}}</label>
<input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md">
</div>
il y a un exemple dans ce lien pour la Version RC5: http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
const noop = () => {
};
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
};
@Component({
selector: 'custom-input',
template: `<div class="form-group">
<label>
<ng-content></ng-content>
<input [(ngModel)]="value"
class="form-control"
(blur)="onBlur()" >
</label>
</div>`,
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class CustomInputComponent implements ControlValueAccessor {
//The internal data model
private innerValue: any = '';
//Placeholders for the callbacks which are later providesd
//by the Control Value Accessor
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
//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;
this.onChangeCallback(v);
}
}
//Set touched on blur
onBlur() {
this.onTouchedCallback();
}
//From ControlValueAccessor interface
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
//From ControlValueAccessor interface
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
//From ControlValueAccessor interface
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
Nous sommes alors en mesure d'utiliser ce contrôle personnalisé comme suit:
<form>
<custom-input name="someValue"
[(ngModel)]="dataModel">
Enter data:
</custom-input>
</form>
L'exemple de Thierry est utile. Voici les importations qui sont nécessaires pour TagsValueAccessor de fonctionner...
import {Directive, Provider} from 'angular2/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR } from 'angular2/common';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {forwardRef} from 'angular2/src/core/di';
C'est assez facile à faire avec ControlValueAccessor
NG_VALUE_ACCESSOR
.
vous pouvez lire cet article pour faire un champ personnalisé simple créer un composant de champ D'entrée personnalisé avec un angle
vous pouvez également résoudre ce problème avec une directive @ViewChild. Cela donne au parent un accès complet à toutes les variables et fonctions des membres d'un enfant injecté.
voir: comment accéder aux champs d'entrée de l'élément de forme injecté
pourquoi créer un nouveau accesseur de valeur quand vous pouvez utiliser le ngModel interne. Chaque fois que vous créez un composant personnalisé qui contient une entrée[ngModel], nous instancions déjà un ControlValueAccessor. Et c'est l'accesseur nous avons besoin.
modèle:
<div class="form-group" [ngClass]="{'has-error' : hasError}">
<div><label>{{label}}</label></div>
<input type="text" [placeholder]="placeholder" ngModel [ngClass]="{invalid: (invalid | async)}" [id]="identifier" name="{{name}}-input" />
</div>
composant:
export class MyInputComponent {
@ViewChild(NgModel) innerNgModel: NgModel;
constructor(ngModel: NgModel) {
//First set the valueAccessor of the outerNgModel
this.outerNgModel.valueAccessor = this.innerNgModel.valueAccessor;
//Set the innerNgModel to the outerNgModel
//This will copy all properties like validators, change-events etc.
this.innerNgModel = this.outerNgModel;
}
}
utiliser comme:
<my-input class="col-sm-6" label="First Name" name="firstname"
[(ngModel)]="user.name" required
minlength="5" maxlength="20"></my-input>