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.

73
demandé sur Cœur 2016-01-22 17:24:53

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 et ngModel / 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>&nbsp;|&nbsp;</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:

71
répondu Thierry Templier 2016-04-26 15:40:55

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>
63
répondu David 2017-08-17 13:55:52

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>
12
répondu Dániel Kis 2016-08-23 11:25:51

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';
5
répondu Blue 2016-03-29 13:11:23

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

1
répondu Mustafa Dwekat 2018-05-24 22:49:46

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é

0
répondu Michael 2017-05-23 12:26:33

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>
0
répondu Nishant 2017-02-28 09:24:04