Entrée de formulaire personnalisé angulaire 2

89

Comment puis-je créer un composant personnalisé qui fonctionnerait comme une <input>balise native ? Je veux que mon contrôle de formulaire personnalisé puisse prendre en charge ngControl, ngForm, [(ngModel)].

Si je comprends bien, je dois implémenter certaines interfaces pour que mon propre contrôle de formulaire fonctionne comme un contrôle natif.

De plus, il semble que la directive ngForm ne se lie que pour les <input>balises, est-ce exact ? Comment puis-je gérer cela?


Laissez-moi vous expliquer pourquoi j'ai besoin de cela. Je veux envelopper plusieurs éléments d'entrée pour les rendre capables de fonctionner ensemble comme une seule entrée. Y a-t-il une autre façon de gérer cela? Encore une fois: je veux rendre ce contrôle comme le contrôle natif. Validation, ngForm, liaison bidirectionnelle ngModel et autres.

ps: J'utilise Typescript.

Maksim Fomin
la source
1
La plupart des réponses sont obsolètes concernant les versions angulaires actuelles. Jetez un œil à stackoverflow.com/a/41353306/2176962
hgoebl

Réponses:

82

En fait, il y a deux choses à mettre en œuvre:

  • Un composant qui fournit la logique de votre composant de formulaire. Ce n'est pas une entrée car il sera fourni par ngModellui-même
  • Une coutume ControlValueAccessorqui implémentera le pont entre ce composant et ngModel/ngControl

Prenons un échantillon. Je souhaite implémenter un composant qui gère une liste de balises pour une entreprise. Le composant permettra d'ajouter et de supprimer des balises. Je souhaite ajouter une validation pour m'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 TagsComponentcomposant définit la logique pour ajouter et supprimer des éléments dans la tagsliste.

@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 un setValueseul (le nom n'est pas important ici). Nous l'utilisons plus tard pour fournir la valeur du ngModelau composant. Ce composant définit un événement pour notifier lorsque l'état du composant (la liste des balises) est mis à jour.

Implémentons maintenant le lien entre ce composant et ngModel/ ngControl. Cela correspond à une directive qui implémente l' ControlValueAccessorinterface. Un fournisseur doit être défini pour cet accesseur de valeur par rapport au NG_VALUE_ACCESSORjeton (n'oubliez pas de l'utiliser forwardRefcar la directive est définie après).

La directive attachera un écouteur d'événement sur l' tagsChangeévénement de l'hôte (c'est-à-dire le composant auquel la directive est attachée, c'est-à-dire le TagsComponent). La onChangeméthode sera appelée lorsque l'événement se produira. Cette méthode correspond à celle enregistrée par Angular2. De cette façon, il sera informé des modifications et met à jour en conséquence le contrôle de formulaire associé.

Le writeValueest appelé lorsque la valeur liée dans le ngFormest mise à jour. Après avoir injecté le composant attaché (ie TagsComponent), nous pourrons l'appeler pour passer cette valeur (voir la setValueméthode précédente ).

N'oubliez pas de fournir le CUSTOM_VALUE_ACCESSORdans les liaisons de la directive.

Voici le code complet de la custom 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, lorsque je supprime tous les éléments tagsde l'entreprise, l' validattribut du companyForm.controls.tagscontrôle devient falseautomatiquement.

Consultez cet article (section "Composant compatible NgModel") pour plus de détails:

Thierry Templier
la source
Merci! Vous êtes génial! Comment pensez-vous - est-ce vraiment bien de cette façon? Je veux dire: n'utilisez pas d'éléments d'entrée et créez vos propres contrôleurs comme: <textfield>, <dropdown>? Est-ce une manière «angulaire»?
Maksim Fomin
1
Je dirais que si vous souhaitez implémenter votre propre champ dans le formulaire (quelque chose de personnalisé), utilisez cette approche. Sinon, utilisez des éléments HTML natifs. Cela dit, si vous souhaitez modulariser la manière d'afficher input / textarea / select (par exemple avec Bootstrap3), vous pouvez exploiter ng-content. Voir cette réponse: stackoverflow.com/questions/34950950/…
Thierry Templier
3
Le code ci-dessus est manquant et présente des différences, comme «removeLabel» au lieu de «removeLabel». Voir ici pour un exemple de fonctionnement complet. Merci Thierry d'avoir donné l'exemple initial!
Bleu
1
Trouvé-le, importez depuis @ angular / forms au lieu de @ angular / common et cela fonctionne. importer {NG_VALUE_ACCESSOR, ControlValueAccessor} depuis '@ angular / forms';
Cagatay Civici
1
ce lien devrait également être utile ..
refactoriser
109

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 et fonctionnel possible. Je l'ai distillé un peu:

HTML pour formulaire externe utilisant le composant implémentant ngModel:

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>

Composant autonome (pas de classe `` accesseur '' 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 de résumer tout cela dans une classe abstraite que j'étends maintenant avec chaque composant dont j'ai besoin pour utiliser ngModel. Pour moi, c'est une tonne de codes généraux et standard dont je peux me passer.

Edit: Le voici:

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>
David
la source
1
Fait intéressant, la réponse acceptée semble avoir cessé de fonctionner depuis RC2, j'ai essayé cette approche et cela fonctionne, je ne sais pas pourquoi.
3urdoch
1
@ 3urdoch Sure, one sec
David
6
Pour le faire fonctionner avec de nouvelles @angular/formsimportations de mise à jour juste: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk
6
Provider () n'est pas pris en charge dans Angular2 Final. Au lieu de cela, faites renvoyer MakeProvider () {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa
2
Vous n'avez plus besoin de les importer CORE_DIRECTIVESet de les ajouter dans le @Componentcar ils sont fournis par défaut maintenant depuis Angular2 final. Cependant, selon mon IDE, "les constructeurs pour les classes dérivées doivent contenir un" super "appel.", Donc j'ai dû ajouter super();au constructeur de mon composant.
Joseph Webber
16

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>
Dániel Kis
la source
4
Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien pour référence. Les réponses aux liens uniquement peuvent devenir invalides si la page liée change.
Maximilian Ast
5

L'exemple de Thierry est utile. Voici les importations nécessaires à l'exécution de TagsValueAccessor ...

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';
Bleu
la source
1

J'ai écrit une bibliothèque qui permet de réduire un peu passe- partout pour ce cas: s-ng-utils. Certaines des autres réponses donnent un exemple d'encapsulation d'un contrôle de formulaire unique . L' utilisation s-ng-utilsqui peut être fait très simplement à l' aide WrappedFormControlSuperclass:

@Component({
    template: `
      <!-- any fancy wrapping you want in the template -->
      <input [formControl]="formControl">
    `,
    providers: [provideValueAccessor(StringComponent)],
})
class StringComponent extends WrappedFormControlSuperclass<string> {
  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }
}

Dans votre message, vous mentionnez que vous souhaitez intégrer plusieurs contrôles de formulaire dans un seul composant. Voici un exemple complet faisant cela avec FormControlSuperclass.

import { Component, Injector } from "@angular/core";
import { FormControlSuperclass, provideValueAccessor } from "s-ng-utils";

interface Location {
  city: string;
  country: string;
}

@Component({
  selector: "app-location",
  template: `
    City:
    <input
      [ngModel]="location.city"
      (ngModelChange)="modifyLocation('city', $event)"
    />
    Country:
    <input
      [ngModel]="location.country"
      (ngModelChange)="modifyLocation('country', $event)"
    />
  `,
  providers: [provideValueAccessor(LocationComponent)],
})
export class LocationComponent extends FormControlSuperclass<Location> {
  location!: Location;

  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }

  handleIncomingValue(value: Location) {
    this.location = value;
  }

  modifyLocation<K extends keyof Location>(field: K, value: Location[K]) {
    this.location = { ...this.location, [field]: value };
    this.emitOutgoingValue(this.location);
  }
}

Vous pouvez ensuite utiliser <app-location>avec [(ngModel)], [formControl], validateurs sur mesure - tout ce que vous pouvez faire avec les contrôles supports angulaires de la boîte.

Eric Simonton
la source
-1

Pourquoi créer un nouvel accesseur de valeur lorsque 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 dont 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;
    }
}

Utilisé comme:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>
Nishant
la source
Bien que cela semble prometteur, puisque vous appelez super, il manque un "extend"
Dave Nottage
1
Oui, je n'ai pas copié tout mon code ici et j'ai oublié de supprimer le super ().
Nishant
9
De plus, d'où vient externalNgModel? Cette réponse serait mieux servie avec le code complet
Dave Nottage
Selon angular.io/docs/ts/latest/api/core/index/… innerNgModel est défini dansngAfterViewInit
Matteo Suppo
2
Cela ne fonctionne pas du tout. innerNgModel n'est jamais initialisé, externalNgModel n'est jamais déclaré et ngModel passé au constructeur n'est jamais utilisé.
user2350838