Angular4 - Kein Wertzugriff für die Formularsteuerung

145

Ich habe ein benutzerdefiniertes Element:

<div formControlName="surveyType">
  <div *ngFor="let type of surveyTypes"
       (click)="onSelectType(type)"
       [class.selected]="type === selectedType">
    <md-icon>{{ type.icon }}</md-icon>
    <span>{{ type.description }}</span>
  </div>
</div>

Wenn ich versuche, den formControlName hinzuzufügen, wird eine Fehlermeldung angezeigt:

FEHLER Fehler: Kein Wertzugriff für die Formularsteuerung mit dem Namen: 'queryType'

Ich habe versucht, ngDefaultControlohne Erfolg hinzuzufügen . Es scheint, weil es keine Eingabe / Auswahl gibt ... und ich weiß nicht, was ich tun soll.

Ich möchte meinen Klick an dieses formControl binden, damit, wenn jemand auf die gesamte Karte klickt, mein 'Typ' in das formControl verschoben wird. Ist es möglich?

jbtd
quelle
Ich weiß nicht, worum es geht: formControl verwendet die Formularsteuerung in HTML, aber div ist keine Formularsteuerung. Ich möchte meinen Umfrage-Typ mit der type.id meiner Karte div
jbtd
Ich weiß, dass ich den alten Winkelweg verwenden und meinen selectedType daran binden lassen könnte, aber ich habe versucht, reaktive Form aus Winkel 4 zu verwenden und zu lernen, und weiß nicht, wie man formControl mit dieser Art von Fall verwendet.
jbtd
Ok, es ist vielleicht so, aber dieser Fall kann nicht von einer reaktiven Form behandelt werden. Danke trotzdem :)
jbtd
Ich habe hier eine Antwort darauf gegeben, wie große Formulare in Unterkomponenten zerlegt werden können. Stackoverflow.com/a/56375605/2398593 Dies gilt jedoch auch sehr gut mit nur einem benutzerdefinierten Steuerwert-Accessor. Überprüfen Sie auch github.com/cloudnc/ngx-sub-form :)
maxime1992

Antworten:

250

Sie können formControlNamenur für Direktiven verwenden, die implementieren ControlValueAccessor.

Implementieren Sie die Schnittstelle

Um das zu tun, was Sie wollen, müssen Sie eine Komponente erstellen, die implementiert wird ControlValueAccessor, was bedeutet , dass die folgenden drei Funktionen implementiert werden :

  • writeValue (teilt Angular mit, wie der Wert aus dem Modell in die Ansicht geschrieben werden soll)
  • registerOnChange (registriert eine Handlerfunktion, die aufgerufen wird, wenn sich die Ansicht ändert)
  • registerOnTouched (Registriert einen Handler, der aufgerufen werden soll, wenn die Komponente ein Berührungsereignis empfängt. Dies ist hilfreich, um festzustellen, ob die Komponente fokussiert wurde.)

Registrieren Sie einen Anbieter

Dann müssen Sie Angular mitteilen, dass es sich bei dieser Direktive um eine handelt ControlValueAccessor(die Schnittstelle wird sie nicht ausschneiden, da sie beim Kompilieren von TypeScript zu JavaScript aus dem Code entfernt wird). Sie tun dies, indem Sie einen Anbieter registrieren .

Der Anbieter sollte einen vorhandenen Wert bereitstellen NG_VALUE_ACCESSORund verwenden . Du brauchst auch ein forwardRefhier. Beachten Sie, dass NG_VALUE_ACCESSORdies ein Multi-Provider sein sollte .

Wenn Ihre benutzerdefinierte Direktive beispielsweise MyControlComponent heißt, sollten Sie innerhalb des an den @ComponentDekorateur übergebenen Objekts etwas in der folgenden Richtung hinzufügen :

providers: [
  { 
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: forwardRef(() => MyControlComponent),
  }
]

Verwendung

Ihre Komponente ist einsatzbereit. Mit Vorlagen basierenden Formen , ngModelBindung wird nun korrekt funktionieren.

Mit reaktiven Formularen können Sie diese jetzt ordnungsgemäß verwenden formControlNameund das Formularsteuerelement verhält sich wie erwartet.

Ressourcen

Lazar Ljubenović
quelle
71

Ich denke du solltest formControlName="surveyType"auf einem inputund nicht auf einem verwendendiv

Vega
quelle
Ja sicher, aber ich weiß nicht, wie ich meine Karte div in etwas anderes verwandeln soll, das eine HTML-Formularsteuerung sein wird
jbtd
5
Der Sinn von CustomValueAccessor ist es, ALLES, sogar ein div
SoEzPz
4
@ SoEzPz Dies ist jedoch ein schlechtes Muster. Sie ahmen die Eingabefunktionalität in einer Wrapper-Komponente nach, implementieren Standard-HTML-Methoden selbst neu (erfinden also das Rad neu und machen Ihren Code ausführlich). In 90% der Fälle können Sie jedoch alles erreichen, was Sie möchten, indem Sie <ng-content>eine Wrapper-Komponente verwenden und die übergeordnete Komponente, die definiert, formControlseinfach den <Eingang> in den <Wrapper> einfügen
Phil
3

Der Fehler bedeutet, dass Angular nicht weiß, was zu tun ist, wenn Sie eine formControlauf eine setzen div. Um dies zu beheben, haben Sie zwei Möglichkeiten.

  1. Sie setzen das formControlNameauf ein Element, das von Angular unterstützt wird, sofort ein. Das sind: input, textareaund select.
  2. Sie implementieren die ControlValueAccessorSchnittstelle. Auf diese Weise teilen Sie Angular mit, "wie Sie auf den Wert Ihres Steuerelements zugreifen können" (daher der Name). Oder in einfachen Worten: Was tun, wenn Sie formControlNameein Element mit einem Wert versehen, dem natürlich kein Wert zugeordnet ist ?

Jetzt kann die Implementierung der ControlValueAccessorSchnittstelle zunächst etwas entmutigend sein. Vor allem, weil es nicht viel gute Dokumentation dafür gibt und Sie Ihrem Code viel Boilerplate hinzufügen müssen. Lassen Sie mich versuchen, dies in einfach zu befolgenden Schritten aufzuschlüsseln.

Verschieben Sie Ihr Formularsteuerelement in eine eigene Komponente

Um das zu implementieren ControlValueAccessor, müssen Sie eine neue Komponente (oder Direktive) erstellen. Verschieben Sie den Code für Ihr Formularsteuerelement dorthin. So wird es auch leicht wiederverwendbar sein. Ein Steuerelement bereits in einer Komponente zu haben, könnte in erster Linie der Grund sein, warum Sie die ControlValueAccessorSchnittstelle implementieren müssen , da Sie Ihre benutzerdefinierte Komponente sonst nicht zusammen mit Angular-Formularen verwenden können.

Fügen Sie das Boilerplate Ihrem Code hinzu

Die Implementierung der ControlValueAccessorSchnittstelle ist recht ausführlich. Hier ist das mitgelieferte Boilerplate:

import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // a) copy paste this providers property (adjust the component name in the forward ref)
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // c) copy paste this code
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // d) copy paste this code
  writeValue(input: string) {
    // TODO
  }

Was machen die einzelnen Teile?

  • a) Lässt Angular zur Laufzeit wissen, dass Sie die ControlValueAccessorSchnittstelle implementiert haben
  • b) Stellt sicher, dass Sie die ControlValueAccessorSchnittstelle implementieren
  • c) Dies ist wahrscheinlich der verwirrendste Teil. Grundsätzlich geben Sie Angular die Möglichkeit, Ihre Klasseneigenschaften / -methoden onChangeund onTouchihre eigene Implementierung zur Laufzeit zu überschreiben , sodass Sie diese Funktionen dann aufrufen können. Daher ist dieser Punkt wichtig zu verstehen: Sie müssen onChange und onTouch nicht selbst implementieren (außer der anfänglichen leeren Implementierung). Das einzige, was Sie mit (c) tun, ist, Angular seine eigenen Funktionen an Ihre Klasse anhängen zu lassen. Warum? Sie können dann die von Angular bereitgestellten Methoden und zum entsprechenden Zeitpunkt aufrufen . Wir werden unten sehen, wie das funktioniert.onChangeonTouch
  • d) Wir werden auch writeValueim nächsten Abschnitt sehen, wie die Methode funktioniert, wenn wir sie implementieren. Ich habe es hier eingefügt, damit alle erforderlichen Eigenschaften ControlValueAccessorimplementiert werden und Ihr Code weiterhin kompiliert wird.

Implementieren Sie writeValue

Was Sie tunwriteValue müssen, ist, etwas in Ihrer benutzerdefinierten Komponente zu tun, wenn das Formularsteuerelement außen geändert wird . Wenn Sie beispielsweise Ihre benutzerdefinierte Formularsteuerungskomponente benannt haben app-custom-inputund diese in der übergeordneten Komponente wie folgt verwenden würden:

<form [formGroup]="form">
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

wird dann writeValueausgelöst, wenn die übergeordnete Komponente den Wert von irgendwie ändert myFormControl. Dies kann beispielsweise während der Initialisierung des Formulars ( this.form = this.formBuilder.group({myFormControl: ""});) oder beim Zurücksetzen eines Formulars geschehen this.form.reset();.

Wenn sich der Wert des Formularsteuerelements äußerlich ändert, möchten Sie ihn normalerweise in eine lokale Variable schreiben, die den Wert des Formularsteuerelements darstellt. Wenn Sie CustomInputComponentsich beispielsweise um ein textbasiertes Formularsteuerelement drehen, könnte dies folgendermaßen aussehen:

writeValue(input: string) {
  this.input = input;
}

und im HTML von CustomInputComponent:

<input type="text"
       [ngModel]="input">

Sie können es auch direkt in das Eingabeelement schreiben, wie in den Angular-Dokumenten beschrieben.

Jetzt haben Sie behandelt, was innerhalb Ihrer Komponente passiert, wenn sich etwas außerhalb ändert. Schauen wir uns jetzt die andere Richtung an. Wie informieren Sie die Außenwelt, wenn sich innerhalb Ihrer Komponente etwas ändert?

OnChange aufrufen

Der nächste Schritt besteht darin, die übergeordnete Komponente über Änderungen in Ihrem Computer zu informieren CustomInputComponent. Hier kommen die onChangeund onTouchFunktionen von (c) von oben ins Spiel. Durch Aufrufen dieser Funktionen können Sie die Außenwelt über Änderungen innerhalb Ihrer Komponente informieren. Um Änderungen des Werts nach außen zu übertragen, müssen Sie onChange mit dem neuen Wert als Argument aufrufen . Wenn der Benutzer beispielsweise inputin Ihrer benutzerdefinierten Komponente etwas in das Feld eingibt, rufen Sie onChangemit dem aktualisierten Wert auf:

<input type="text"
       [ngModel]="input"
       (ngModelChange)="onChange($event)">

Wenn Sie die Implementierung (c) erneut von oben überprüfen, werden Sie sehen, was passiert: Angular hat seine eigene Implementierung an die onChangeclass -Eigenschaft gebunden . Diese Implementierung erwartet ein Argument, nämlich den aktualisierten Steuerwert. Was Sie jetzt tun, ist, dass Sie diese Methode aufrufen und Angular über die Änderung informieren. Angular ändert nun den Formularwert an der Außenseite. Dies ist der Schlüssel zu all dem. Sie haben Angular durch Aufrufen mitgeteilt, wann und mit welchem ​​Wert das Formularsteuerelement aktualisiert werden sollonChange . Sie haben ihm die Möglichkeit gegeben, "auf den Steuerwert zuzugreifen".

Übrigens: Der Name onChange wird von mir gewählt. Sie können hier beispielsweise alles propagateChangeoder ähnliches auswählen . Wie auch immer Sie es nennen, es ist dieselbe Funktion, die ein Argument akzeptiert, das von Angular bereitgestellt wird und das zur registerOnChangeLaufzeit von der Methode an Ihre Klasse gebunden wird .

OnTouch anrufen

Da Formularsteuerelemente "berührt" werden können, sollten Sie Angular auch die Möglichkeit geben, zu verstehen, wann Ihr benutzerdefiniertes Formularsteuerelement berührt wird. Sie können es tun, Sie haben es erraten, indem Sie die onTouchFunktion aufrufen . Wenn Sie in unserem Beispiel hier die Anforderungen von Angular für die sofort einsatzbereiten Formularsteuerelemente erfüllen möchten, sollten Sie Folgendes aufrufen, onTouchwenn das Eingabefeld unscharf ist:

<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">

Nochmal, onTouch ist ein Name von mir gewählt, aber was seine eigentliche Funktion ist, wird von Angular bereitgestellt und es werden keine Argumente verwendet. Was Sinn macht, da Sie Angular nur wissen lassen, dass das Formularsteuerelement berührt wurde.

Alles zusammenfügen

Wie sieht das aus, wenn alles zusammenkommt? Es sollte so aussehen:

// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // Step 1: copy paste this providers property
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // Step 3: Copy paste this stuff here
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // Step 4: Define what should happen in this component, if something changes outside
  input: string;
  writeValue(input: string) {
    this.input = input;
  }

  // Step 5: Handle what should happen on the outside, if something changes on the inside
  // in this simple case, we've handled all of that in the .html
  // a) we've bound to the local variable with ngModel
  // b) we emit to the ouside by calling onChange on ngModelChange

}
// custom-input.component.html
<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>

// OR

<form [formGroup]="form" >
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

Mehr Beispiele

Verschachtelte Formulare

Beachten Sie, dass Control Value Accessors NICHT das richtige Werkzeug für verschachtelte Formulargruppen sind. Für verschachtelte Formulargruppen können Sie @Input() subformstattdessen einfach ein verwenden. Control Value Accessors sollen einwickeln controls, nicht groups! In diesem Beispiel wird gezeigt, wie eine Eingabe für ein verschachteltes Formular verwendet wird: https://stackblitz.com/edit/angular-nested-forms-input-2

Quellen

Bersling
quelle
-1

Für mich war dies auf das Attribut "multiple" bei der Auswahl der Eingabesteuerung zurückzuführen, da Angular für diesen Steuerungstyp einen anderen ValueAccessor hat.

const countryControl = new FormControl();

Und innerhalb der Vorlage verwenden Sie so

    <select multiple name="countries" [formControl]="countryControl">
      <option *ngFor="let country of countries" [ngValue]="country">
       {{ country.name }}
      </option>
    </select>

Weitere Details finden Sie unter Offizielle Dokumente

Sudhir Singh
quelle
Was war auf "mehrere" zurückzuführen? Ich sehe nicht, wie Ihr Code irgendetwas löst oder was das ursprüngliche Problem war. Ihr Code zeigt die übliche Grundverwendung.
Lazar Ljubenović