web-dev-qa-db-fra.com

Entrée de formulaire personnalisé angulaire 2

Comment créer un composant personnalisé qui fonctionnerait exactement comme une balise native <input>? Je souhaite 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 système natif. 

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


Laissez-moi vous expliquer pourquoi j'ai besoin de ça du tout. Je veux envelopper plusieurs éléments d'entrée pour les rendre capables de fonctionner ensemble en une seule entrée. Existe-t-il un autre moyen de gérer cela? Une fois de plus: je veux que ce contrôle soit identique à un contrôle natif. Validation, ngForm, liaison bidirectionnelle ngModel et autre.

ps: j'utilise TypeScript.

75
Maksim Fomin

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 puisqu'elle sera fournie par ngModel elle-même
  • Une ControlValueAccessor personnalisée qui 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 tags 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 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 constater, cette composante n’a pas d’entrée, mais une variable setValue (le nom n’est pas important ici). Nous l'utilisons plus tard pour fournir la valeur de la variable ngModel au composant. Ce composant définit un événement à notifier lorsque son état (la liste de balises) 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 cet accesseur de valeur par rapport au jeton NG_VALUE_ACCESSOR (n'oubliez pas d'utiliser forwardRef car la directive est définie après).

La directive attache un écouteur d’événement à l’événement tagsChange de l’hôte (c’est-à-dire le composant auquel la directive est attachée, c’est-à-dire la TagsComponent). La méthode onChange 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 mettra à jour le contrôle de formulaire associé.

La writeValue est appelée lorsque la valeur liée à la ngForm est mise à jour. Après avoir injecté le composant attaché (par exemple, TagsComponent), nous pourrons l’appeler pour transmettre cette valeur (voir la méthode précédente setValue).

N'oubliez pas de fournir le CUSTOM_VALUE_ACCESSOR dans les liaisons 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; }
}

Ainsi, lorsque je supprime toute la tags de la société, l'attribut valid du contrôle companyForm.controls.tags devient automatiquement false.

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

73
Thierry Templier

Je ne comprends pas pourquoi chaque exemple que je trouve sur Internet doit être si compliqué. En expliquant 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 formulaire externe utilisant un 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 je développe maintenant avec chaque composant nécessaire pour utiliser ngModel. Pour moi, il s'agit d'une tonne de code aérien et passe-partout 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>
76
David

Voici 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 pouvons ensuite utiliser ce contrôle personnalisé comme suit:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
17
Dániel Kis

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';
5
Blue

J'ai écrit une bibliothèque qui aide à réduire certains problèmes dans ce cas: s-ng-utils . Certaines des autres réponses donnent des exemples d'encapsulation d'un contrôle single form. Utiliser s-ng-utils peut être fait très simplement avec 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 indiquez que vous souhaitez regrouper 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], des validateurs personnalisés - tout ce que vous pouvez faire avec les commandes Angular prend en charge les opérations immédiatement.

0
Eric Simonton

Vous pouvez également résoudre ce problème avec une directive @ViewChild. Cela donne au parent un accès complet à toutes les variables membres et aux fonctions d'un enfant injecté.

Voir: Comment accéder aux champs de saisie du composant de formulaire injecté

0
Michael