Benutzerdefinierte Angular 2-Formulareingabe

88

Wie kann ich eine benutzerdefinierte Komponente erstellen, die genau wie ein natives <input>Tag funktioniert ? Ich möchte, dass mein benutzerdefiniertes Formularsteuerelement ngControl, ngForm, [(ngModel)] unterstützt.

Soweit ich weiß, muss ich einige Schnittstellen implementieren, damit meine eigene Formularsteuerung genauso funktioniert wie die native.

Außerdem scheint die ngForm-Direktive nur für <input>Tags zu binden. Ist das richtig? Wie kann ich damit umgehen?


Lassen Sie mich erklären, warum ich das überhaupt brauche. Ich möchte mehrere Eingabeelemente umschließen, damit sie als eine einzige Eingabe zusammenarbeiten können. Gibt es eine andere Möglichkeit, damit umzugehen? Noch einmal: Ich möchte dieses Steuerelement genauso wie das native steuern. Validierung, ngForm, ngModel Zwei-Wege-Bindung und andere.

ps: Ich benutze Typescript.

Maksim Fomin
quelle
1
Die meisten Antworten sind in Bezug auf aktuelle Angular-Versionen veraltet. Werfen
hgoebl

Antworten:

82

In der Tat gibt es zwei Dinge zu implementieren:

  • Eine Komponente, die die Logik Ihrer Formularkomponente bereitstellt. Es ist keine Eingabe, da es von ngModelselbst bereitgestellt wird
  • Ein Benutzer ControlValueAccessor, der die Brücke zwischen dieser Komponente und ngModel/ implementiertngControl

Nehmen wir eine Probe. Ich möchte eine Komponente implementieren, die eine Liste von Tags für ein Unternehmen verwaltet. Die Komponente ermöglicht das Hinzufügen und Entfernen von Tags. Ich möchte eine Validierung hinzufügen, um sicherzustellen, dass die Tag-Liste nicht leer ist. Ich werde es in meiner Komponente wie unten beschrieben definieren:

(...)
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]
    });
  }
}

Die TagsComponentKomponente definiert die Logik zum Hinzufügen und Entfernen von Elementen in der 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 = '';
  }
}

Wie Sie sehen können, gibt es in dieser Komponente keine Eingabe, sondern eine setValueEins (der Name ist hier nicht wichtig). Wir verwenden es später, um den Wert von der ngModelzur Komponente bereitzustellen . Diese Komponente definiert ein Ereignis, das benachrichtigt werden soll, wenn der Status der Komponente (die Tags-Liste) aktualisiert wird.

Lassen Sie uns nun die Verknüpfung zwischen dieser Komponente und ngModel/ implementieren ngControl. Dies entspricht einer Direktive, die die ControlValueAccessorSchnittstelle implementiert . Für diesen Wert-Accessor muss ein Provider für das NG_VALUE_ACCESSORToken definiert werden (vergessen Sie nicht, ihn zu verwenden, forwardRefda die Direktive danach definiert ist).

Die Direktive fügt dem tagsChangeEreignis des Hosts einen Ereignis-Listener hinzu (dh die Komponente, an die die Direktive angehängt ist, dh die TagsComponent). Die onChangeMethode wird aufgerufen, wenn das Ereignis eintritt. Diese Methode entspricht der von Angular2 registrierten. Auf diese Weise werden Änderungen erkannt und das zugehörige Formularsteuerelement entsprechend aktualisiert.

Das writeValuewird aufgerufen, wenn der in gebundene Wert ngFormaktualisiert wird. Nachdem Sie die angehängte Komponente (dh TagsComponent) eingefügt haben, können Sie sie aufrufen, um diesen Wert zu übergeben (siehe vorherige setValueMethode).

Vergessen Sie nicht, die CUSTOM_VALUE_ACCESSORin den Bindungen der Richtlinie anzugeben.

Hier ist der vollständige Code des Brauchs 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; }
}

Auf diese Weise tagswird das validAttribut des companyForm.controls.tagsSteuerelements falseautomatisch , wenn ich das gesamte Unternehmen entferne .

Weitere Informationen finden Sie in diesem Artikel (Abschnitt "NgModel-kompatible Komponente"):

Thierry Templier
quelle
Vielen Dank! Du bist unglaublich! Wie denkst du - ist dieser Weg eigentlich in Ordnung? Ich meine: Verwenden Sie keine Eingabeelemente und erstellen Sie eigene Steuerelemente wie: <textfield>, <dropdown>? Ist das "eckig"?
Maksim Fomin
1
Ich würde sagen, wenn Sie Ihr eigenes Feld in dem Formular implementieren möchten (etwas Benutzerdefiniertes), verwenden Sie diesen Ansatz. Verwenden Sie andernfalls native HTML-Elemente. Das heißt, wenn Sie die Art und Weise der Anzeige von Eingabe / Textbereich / Auswahl modularisieren möchten (z. B. mit Bootstrap3), können Sie ng-content nutzen. Siehe diese Antwort: stackoverflow.com/questions/34950950/…
Thierry Templier
3
Der obige Code fehlt und weist einige Unstimmigkeiten auf, z. B. 'removeLabel' anstelle von 'removeLabel'. Ein vollständiges Arbeitsbeispiel finden Sie hier . Vielen Dank an Thierry für das erste Beispiel!
Blue
1
Gefunden, aus @ angle / forms anstelle von @ angle / common importieren und es funktioniert. {NG_VALUE_ACCESSOR, ControlValueAccessor} aus '@ angle / forms' importieren;
Cagatay Civici
1
Dieser Link sollte auch hilfreich sein ..
Refactor
108

Ich verstehe nicht, warum jedes Beispiel, das ich im Internet finde, so kompliziert sein muss. Wenn ich ein neues Konzept erkläre, denke ich, ist es immer am besten, ein möglichst einfaches und funktionierendes Beispiel zu haben. Ich habe es ein wenig destilliert:

HTML für externes Formular mit Komponente, die ngModel implementiert:

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

In sich geschlossene Komponente (keine separate 'Accessor'-Klasse - vielleicht fehlt mir der Punkt):

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; }
}

Tatsächlich habe ich all diese Dinge gerade zu einer abstrakten Klasse abstrahiert, die ich jetzt mit jeder Komponente erweitere, die ich zur Verwendung von ngModel benötige. Für mich ist dies eine Menge Overhead- und Boilerplate-Code, auf den ich verzichten kann.

Edit: Hier ist es:

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
  };
}

Hier ist eine Komponente, die es verwendet: (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
quelle
1
Interessanterweise scheint die akzeptierte Antwort seit RC2 nicht mehr zu funktionieren. Ich habe diesen Ansatz ausprobiert und er funktioniert, nicht sicher warum.
3urdoch
1
@ 3urdoch Sicher, eine Sekunde
David
6
Damit es mit neuen funktioniert, @angular/formsaktualisieren Sie einfach die Importe: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk
6
Provider () wird in Angular2 Final nicht unterstützt. Lassen Sie stattdessen MakeProvider () zurückgeben {bereitstellen: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa
2
Sie müssen sie nicht mehr importieren CORE_DIRECTIVESund hinzufügen, @Componentda sie seit Angular2 final standardmäßig bereitgestellt werden. Laut meiner IDE müssen "Konstruktoren für abgeleitete Klassen einen 'Super'-Aufruf enthalten.", Daher musste super();ich den Konstruktor meiner Komponente ergänzen .
Joseph Webber
16

In diesem Link gibt es ein Beispiel für die RC5-Version: 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;
    }

}

Wir können dieses benutzerdefinierte Steuerelement dann wie folgt verwenden:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
Dániel Kis
quelle
4
Während dieser Link die Frage beantworten kann, ist es besser, die wesentlichen Teile der Antwort hier aufzunehmen und den Link als Referenz bereitzustellen. Nur-Link-Antworten können ungültig werden, wenn sich die verknüpfte Seite ändert.
Maximilian Ast
5

Thierrys Beispiel ist hilfreich. Hier sind die Importe, die für die Ausführung von TagsValueAccessor erforderlich sind ...

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';
Blau
quelle
1

Ich habe eine Bibliothek geschrieben, die hilft, einige Boilerplates für diesen Fall zu reduzieren : s-ng-utils. Einige der anderen Antworten geben ein Beispiel für das Umschließen eines einzelnen Formularsteuerelements. Dies s-ng-utilskann sehr einfach erfolgen mit 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);
  }
}

In Ihrem Beitrag erwähnen Sie, dass Sie mehrere Formularsteuerelemente in eine einzelne Komponente einbinden möchten. Hier ist ein vollständiges Beispiel dafür 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);
  }
}

Anschließend können Sie <app-location>mit [(ngModel)], [formControl], benutzerdefinierte Validatoren - alles , was Sie mit den Kontrollen Winkelstützen aus dem Kasten heraus tun.

Eric Simonton
quelle
-1

Sie können dies auch mit einer @ ViewChild-Direktive lösen. Dies gibt dem Elternteil vollen Zugriff auf alle Mitgliedsvariablen und Funktionen eines injizierten Kindes.

Siehe: Zugriff auf Eingabefelder der injizierten Formularkomponente

Michael
quelle
Klingt nach einem Hack :(
Realappie
-1

Warum einen neuen Wert-Accessor erstellen, wenn Sie das innere ngModel verwenden können? Wenn Sie eine benutzerdefinierte Komponente erstellen, die eine Eingabe [ngModel] enthält, instanziieren wir bereits einen ControlValueAccessor. Und das ist der Accessor, den wir brauchen.

Vorlage:

<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>

Komponente:

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;
    }
}

Benutzen als:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>
Nishant
quelle
Während dies vielversprechend aussieht, da Sie super anrufen, fehlt eine "Verlängerung"
Dave Nottage
1
Ja, ich habe nicht meinen gesamten Code hierher kopiert und vergessen, das super () zu entfernen.
Nishant
9
Woher kommt OuterNgModel? Diese Antwort wäre besser mit vollständigem Code
Dave Nottage
Laut angular.io/docs/ts/latest/api/core/index/… innerNgModel ist inngAfterViewInit
Matteo Suppo
2
Das funktioniert überhaupt nicht. innerNgModel wird niemals initialisiert, OuterNgModel wird niemals deklariert und ngModel, das an den Konstruktor übergeben wird, wird niemals verwendet.
user2350838