Angular4 - Brak metody dostępu do wartości dla kontroli formularza

146

Mam element niestandardowy:

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

Kiedy próbuję dodać formControlName, pojawia się komunikat o błędzie:

BŁĄD Błąd: brak metody dostępu wartości dla kontrolki formularza o nazwie: „SurveyType”

Próbowałem dodać ngDefaultControlbez powodzenia. Wydaje się, że to dlatego, że nie ma wejścia / wyboru ... i nie wiem, co robić.

Chciałbym powiązać moje kliknięcie z tym formControl, aby gdy ktoś kliknął na całą kartę, wypchnąłby mój „typ” do formControl. Czy to możliwe?

jbtd
źródło
Nie wiem, o co mi chodzi: formControl idzie do kontroli formularza w html, ale div nie jest kontrolką formularza. Chciałbym powiązać mój typ ankiety z type.id mojej karty div
jbtd
Wiem, że mógłbym użyć starego sposobu kątowego i powiązać z nim mój selectedType, ale próbowałem użyć i nauczyć się formy reaktywnej z angular 4 i nie wiem, jak używać formControl w tego typu przypadkach.
jbtd
Ok, to może tylko tyle, że sprawa nie może być obsłużona przez formularz reaktywny, więc. Dzięki mimo wszystko :)
jbtd
Zrobiłem tutaj odpowiedź o tym, jak rozbić ogromne formy na komponenty podrzędne stackoverflow.com/a/56375605/2398593, ale odnosi się to również bardzo dobrze do niestandardowego akcesorium wartości kontrolnej. Sprawdź również github.com/cloudnc/ngx-sub-form :)
maxime1992

Odpowiedzi:

250

Możesz używać formControlNametylko w dyrektywach, które implementująControlValueAccessor .

Zaimplementuj interfejs

Aby więc robić to, co chcesz, musisz stworzyć komponent, który implementuje ControlValueAccessor, co oznacza implementację następujących trzech funkcji :

  • writeValue (informuje Angulara, jak zapisać wartość z modelu do widoku)
  • registerOnChange (rejestruje funkcję obsługi, która jest wywoływana, gdy zmienia się widok)
  • registerOnTouched (rejestruje procedurę obsługi, która ma zostać wywołana, gdy komponent otrzyma zdarzenie dotykowe, przydatne do określenia, czy komponent został skupiony).

Zarejestruj dostawcę

Następnie musisz powiedzieć Angularowi, że ta dyrektywa to ControlValueAccessor (interfejs nie będzie jej wycinał, ponieważ jest usuwany z kodu, gdy TypeScript jest kompilowany do JavaScript). Robisz to rejestrując dostawcę .

Dostawca powinien zapewnić NG_VALUE_ACCESSORi wykorzystać istniejącą wartość . Będziesz także potrzebować plikuforwardRef tutaj. Pamiętaj, że NG_VALUE_ACCESSORpowinien to być wielu dostawców .

Na przykład, jeśli twoja niestandardowa dyrektywa nosi nazwę MyControlComponent, powinieneś dodać coś w następujących wierszach wewnątrz obiektu przekazanego do @Componentdekoratora:

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

Stosowanie

Twój komponent jest gotowy do użycia. Z form opartych na szablonach , ngModelwiążąca będzie teraz działać poprawnie.

Dzięki formularzom reaktywnym możesz teraz prawidłowo używać, formControlNamea formant formularza będzie działał zgodnie z oczekiwaniami.

Zasoby

Lazar Ljubenović
źródło
72

Myślę, że powinieneś używać formControlName="surveyType"na inputa nie nadiv

Vega
źródło
Tak,
jasne
5
Celem CustomValueAccessor jest dodanie kontroli formularza do WSZYSTKIEGO, nawet elementu div
SoEzPz
4
@SoEzPz To jest jednak zły wzór. Naśladujesz funkcje wprowadzania danych w komponencie opakowującym, ponownie implementując standardowe metody HTML samodzielnie (w ten sposób zasadniczo od nowa wymyślając koło i tworząc rozwlekły kod). ale w 90% przypadków możesz osiągnąć wszystko, co chcesz, używając <ng-content>komponentu opakowania i pozwolić komponentowi nadrzędnemu, który definiuje, formControlspo prostu umieścić <input> wewnątrz <wrapper>
Phil.
3

Błąd oznacza, że ​​Angular nie wie, co zrobić, gdy postawisz formControlna div. Aby to naprawić, masz dwie możliwości.

  1. Umieszczasz formControlNameelement na elemencie, który jest obsługiwany przez Angular po wyjęciu z pudełka. Są to: input, textareai select.
  2. Ty implementujesz ControlValueAccessorinterfejs. W ten sposób mówisz Angularowi "jak uzyskać dostęp do wartości twojej kontrolki" (stąd nazwa). Lub w prostych słowach: co zrobić, gdy umieścisz element formControlNamena elemencie, który nie ma naturalnie związanej z nim wartości.

Teraz wdrożenie ControlValueAccessorinterfejsu może być na początku nieco zniechęcające. Zwłaszcza, że ​​nie ma tam dobrej dokumentacji na ten temat i musisz dodać dużo schematu do swojego kodu. Pozwólcie, że spróbuję to wyjaśnić w kilku prostych krokach.

Przenieś kontrolkę formularza do jej własnego składnika

Aby zaimplementować ControlValueAccessor, musisz stworzyć nowy komponent (lub dyrektywę). Przenieś tam kod związany z formantem formularza. W ten sposób będzie również łatwo wielokrotnego użytku. Posiadanie kontrolki już wewnątrz komponentu może być przede wszystkim powodem, dla którego musisz zaimplementowaćControlValueAccessor interfejs, ponieważ w przeciwnym razie nie będziesz mógł używać swojego komponentu niestandardowego razem z formularzami Angular.

Dodaj szablon do swojego kodu

Implementacja ControlValueAccessorinterfejsu jest dość rozwlekła, oto standardowy szablon, który z nim związany:

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
  }

Więc co robią poszczególne części?

  • a) Pozwala Angularowi wiedzieć, że ControlValueAccessorinterfejs został zaimplementowany
  • b) Upewnij się, że implementujesz ControlValueAccessorinterfejs
  • c) To jest prawdopodobnie najbardziej zagmatwana część. Zasadniczo to, co robisz, polega na tym, że dajesz Angularowi środki do nadpisywania właściwości / metod klasy onChangei onTouchjego własnej implementacji w czasie wykonywania, dzięki czemu możesz wywoływać te funkcje. Więc ten punkt jest ważny, aby zrozumieć: nie musisz samodzielnie implementować onChange i onTouch (poza początkową pustą implementacją). Jedyne, co robisz z (c), to pozwolić Angularowi dołączyć jego własne funkcje do twojej klasy. Czemu? Możesz więc zadzwonić pod numeronChange ionTouch metod przewidzianych przez kątowej w odpowiednim czasie. Zobaczymy, jak to działa poniżej.
  • d) W writeValuenastępnej sekcji zobaczymy, jak działa ta metoda, kiedy ją zaimplementujemy. Umieściłem to tutaj, więc wszystkie wymagane właściwości ControlValueAccessorsą zaimplementowane, a kod nadal się kompiluje.

Zaimplementuj writeValue

Co writeValuerobi, to zrobić coś wewnątrz komponentu niestandardowego, gdy kontrolka formularza zostanie zmieniona na zewnątrz . Na przykład, jeśli nazwałeś swój komponent kontroli formularza niestandardowego app-custom-inputi chciałbyś go używać w komponencie nadrzędnym w następujący sposób:

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

następnie writeValuejest wyzwalany za każdym razem, gdy komponent nadrzędny w jakiś sposób zmienia wartość myFormControl. Może to być na przykład podczas inicjowania formularza ( this.form = this.formBuilder.group({myFormControl: ""});) lub resetowania formularza this.form.reset();.

Zwykle będziesz chciał zrobić, jeśli wartość kontrolki formularza zmieni się na zewnątrz, to zapisanie jej w zmiennej lokalnej, która reprezentuje wartość kontrolki formularza. Na przykład, jeśli CustomInputComponentobraca się wokół kontrolki formularza opartego na tekście, może to wyglądać następująco:

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

oraz w html CustomInputComponent:

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

Możesz również napisać go bezpośrednio do elementu wejściowego, jak opisano w dokumentacji Angular.

Teraz poradziłeś sobie z tym, co dzieje się wewnątrz komponentu, gdy coś zmienia się na zewnątrz. Spójrzmy teraz w innym kierunku. W jaki sposób informujesz świat zewnętrzny, gdy coś zmienia się w twoim elemencie?

Calling onChange

Następnym krokiem jest poinformowanie komponentu nadrzędnego o zmianach w twoim CustomInputComponent. W tym miejscu do gry wchodzą funkcje onChangei onTouchz (c) z góry. Wywołując te funkcje, możesz informować zewnątrz o zmianach w twoim komponencie. Aby propagować zmiany wartości na zewnątrz, musisz wywołać onChange z nową wartością jako argumentem . Na przykład, jeśli użytkownik wpisze coś w inputpolu komponentu niestandardowego, wywołasz onChangezaktualizowaną wartość:

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

Jeśli ponownie sprawdzisz implementację (c) z góry, zobaczysz, co się dzieje: Angular powiązał swoją własną implementację z onChangewłaściwością klasy. Ta implementacja oczekuje jednego argumentu, który jest zaktualizowaną wartością kontrolną. To, co teraz robisz, to wywołanie tej metody, a tym samym poinformowanie Angulara o zmianie. Angular przejdzie teraz dalej i zmieni wartość formularza na zewnątrz. To jest kluczowa część tego wszystkiego. Poinformowałeś Angular, kiedy powinien zaktualizować kontrolkę formularza i jaką wartość, wywołująconChange . Dałeś mu sposób „dostępu do wartości kontrolnej”.

Przy okazji: nazwa onChangejest wybrana przeze mnie. Możesz tu wybrać cokolwiek, na przykład propagateChangelub coś podobnego. Jednak jakkolwiek ją nazwiesz, będzie to ta sama funkcja, która pobiera jeden argument, który jest dostarczany przez Angular i który jest powiązany z twoją klasą przez registerOnChangemetodę w czasie wykonywania.

Wywołanie onTouch

Ponieważ kontrolki formularza można „dotykać”, należy również dać Angularowi środki do zrozumienia, kiedy dotknięta zostanie niestandardowa kontrolka formularza. Możesz to zrobić, zgadłeś, wywołując onTouchfunkcję. Więc w naszym przykładzie tutaj, jeśli chcesz zachować zgodność z tym, jak Angular robi to dla gotowych kontrolek formularza, powinieneś zadzwonić, onTouchgdy pole wejściowe jest rozmyte:

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

Ponownie, onTouchto nazwa wybrana przeze mnie, ale to, jaka jest jego rzeczywista funkcja, jest dostarczana przez Angular i nie przyjmuje żadnych argumentów. Co ma sens, ponieważ po prostu informujesz Angulara, że ​​kontrolka formularza została dotknięta.

Kładąc wszystko razem

Jak to wygląda, gdy wszystko idzie razem? To powinno wyglądać tak:

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

Więcej przykładów

Zagnieżdżone formularze

Należy zauważyć, że Accessors wartości kontrolnych NIE są odpowiednim narzędziem dla zagnieżdżonych grup formularzy. W przypadku zagnieżdżonych grup formularzy możesz po prostu użyć @Input() subformzamiast tego. Accessors wartości kontrolnych mają na celu zawijanie controls, a nie groups! Zobacz ten przykład, jak używać danych wejściowych dla zagnieżdżonego formularza: https://stackblitz.com/edit/angular-nested-forms-input-2

Źródła

bersling
źródło
-1

Dla mnie było to spowodowane atrybutem „multiple” w kontrolce wyboru wejścia, ponieważ Angular ma inny ValueAccessor dla tego typu kontroli.

const countryControl = new FormControl();

A wewnątrz szablonu użyj w ten sposób

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

Więcej szczegółów w Official Docs

Sudhir Singh
źródło
Co było spowodowane „wieloma”? Nie wiem, jak twój kod cokolwiek rozwiązuje ani jaki był pierwotny problem. Twój kod pokazuje zwykłe podstawowe użycie.
Lazar Ljubenović