javascript - Angular4 - Form kontrolü için değer erişici yok


Benim özel bir öğem var :

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

formControlName eklemeye çalıştığımda, bir hata mesajı alıyorum:

HATA Hata: 'surveyType' adlı form kontrolü için değer alıcı bulunamadı

ngDefaultControl eklemeyi denedim ama başarılı olamadım. Görünüşe göre bunun nedeni input/select olmaması... ve ne yapacağımı bilmiyorum.

Tüm karta tıklandığında 'type'ımı formControl'e göndermesi için tıklamamı bu formControl'e bağlamak istiyorum. Bu mümkün mü?

Cevap #1

formControlName öğesini yalnızca ControlValueAccessor uygulayan direktiflerde kullanabilirsiniz.

Arayüzü uygula

Yani, yapmak istediğin şeyi gerçekleştirmek için ControlValueAccessor uygulayan bir bileşen oluşturman gerekiyor, bu da şu üç fonksiyonu uygulamak anlamına geliyor:

  • writeValue (Angular’a modelden görünümü yazarken değeri nasıl yazacağını söyler)
  • registerOnChange (görünüm değiştiğinde çağrılan bir işleyici fonksiyon kaydeder)
  • registerOnTouched (bileşen bir dokunma olayı aldığında çağrılacak bir işleyici kaydeder, bileşenin odaklanıp odaklanmadığını bilmek için kullanışlıdır).

Bir sağlayıcı kaydol

Sonra, Angular'a bu direktifin bir ControlValueAccessor olduğunu söylemeniz gerekir (arayüz yeterli olmayacaktır çünkü TypeScript JavaScript'e derlendiğinde koddan çıkarılır). Bunu bir sağlayıcı kaydederek yaparsınız.

Sağlayıcı, NG_VALUE_ACCESSOR sağlamalı ve mevcut bir değeri kullanmalıdır. Burada ayrıca bir forwardRef gerekir. NG_VALUE_ACCESSOR bir çoklu sağlayıcı olmalıdır.

Örneğin, özel direktifinizin adı MyControlComponent ise, @Component dekoratörüne geçirilen nesnenin içine aşağıdakine benzer bir şey eklemelisiniz:

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

Kullanım

Bileşeniniz kullanıma hazır. Şablon tabanlı formlarda, ngModel bağlaması artık düzgün çalışacaktır.

Tepkisel formlarla artık formControlName'i düzgün bir şekilde kullanabilir ve form kontrolü beklendiği gibi davranacaktır.

Kaynaklar

  • Angular'da Özel Form Kontrolleri by Thoughtram
  • Reactive Formlar ve NgModel ile Angular Özel Form Kontrolleri yazarı Cory Rylan

Cevap #2

formControlName="surveyType"'i bir div yerine bir input üzerinde kullanmalısınız

Cevap #3

Hata, Angular'ın bir div üzerine bir formControl koyduğunuzda ne yapacağını bilmediği anlamına gelir. Bunu düzeltmek için iki seçeneğiniz vardır.

  1. formControlName'i Angular tarafından kutudan çıktığı gibi desteklenen bir öğeye koyarsınız. Bunlar: input, textarea ve select öğeleridir.
  2. ControlValueAccessor arayüzünü uygularsınız. Bunu yaparak Angular'a "kontrolünüzün değerine nasıl erişileceğini" söylüyorsunuz (bu yüzden adı bu). Veya basit bir ifadeyle: Doğal olarak bir değere sahip olmayan bir öğeye formControlName koyduğunuzda ne yapılacağını belirtir.

Şimdi, ControlValueAccessor arayüzünü uygulamak başta biraz göz korkutucu olabilir. Özellikle, bununla ilgili pek iyi dokümantasyon olmadığı ve kodunuza çok fazla tekrar kod (boilerplate) eklemeniz gerektiği için. Bu yüzden bunu takip etmesi kolay bazı adımlara ayırmaya çalışayım.

Form denetiminizi kendi bileşenine taşıyın

ControlValueAccessor'ı uygulamak için yeni bir bileşen (veya direktif) oluşturmanız gerekir. Form kontrolünüze ait kodu oraya taşıyın. Bu şekilde aynı zamanda kolayca yeniden kullanılabilir olacaktır. Bir bileşenin içinde zaten bir kontrol bulunması, aslında ControlValueAccessor arabirimini uygulamanızın gerekmesinin nedeni olabilir, çünkü aksi takdirde özel bileşeninizi Angular formlarıyla birlikte kullanamazsınız.

Kodunuza hazır şablonu ekleyin

ControlValueAccessor arayüzünü uygulamak oldukça ayrıntılıdır, işte bununla birlikte gelen şablon kod:

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
  }

Peki bireysel parçalar ne yapıyor?

  • a) Çalışma zamanı sırasında Angular’a ControlValueAccessor arayüzünü uyguladığınızı bildirin
  • b) ControlValueAccessor arayüzünü uyguladığınızdan emin olur
  • c) Bu muhtemelen en kafa karıştırıcı kısım. Temelde yaptığınız şey, Angular'a sınıf özelliklerinizi/metodlarınızı onChange ve onTouch kendi uygulamasıyla çalışma zamanında geçersiz kılma imkânı tanımaktır, böylece bu fonksiyonları daha sonra çağırabilirsiniz. Bu noktayı anlamak önemlidir: onChange ve onTouch metodlarını kendiniz uygulamanıza gerek yoktur (ilk baştaki boş uygulama dışında). (c) ile yaptığınız tek şey, Angular'ın kendi fonksiyonlarını sınıfınıza eklemesine izin vermektir. Neden? Böylece Angular tarafından sağlanan onChange ve onTouch metodlarını uygun zamanda çağırabilirsiniz. Bunun nasıl çalıştığını aşağıda göreceğiz.
  • d) Ayrıca bir sonraki bölümde, onu uyguladığımızda writeValue yönteminin nasıl çalıştığını göreceğiz. Bunu buraya koydum, böylece ControlValueAccessor üzerindeki tüm gerekli özellikler uygulanmış olur ve kodunuz hâlâ derlenir.

writeValue yöntemini uygula

writeValue yaptığı şey, form kontrolü dışarıdan değiştirildiğinde özel bileşeninizin içinde bir şeyler yapmaktır. Örneğin, özel form kontrolü bileşeninize app-custom-input adını verdiyseniz ve bunu üst bileşende şöyle kullanıyor olsaydınız:

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

daha sonra writeValue, üst bileşen bir şekilde myFormControl değerini değiştirdiğinde tetiklenir. Bu, örneğin formun başlatılması sırasında (this.form=this.formBuilder.group({myFormControl: ""});) veya bir form sıfırlaması sırasında this.form.reset(); olabilir.

Form kontrolünün dışarıdan değeri değiştiğinde genellikle yapmak isteyeceğiniz şey, form kontrolü değerini temsil eden yerel bir değişkene yazmaktır. Örneğin, CustomInputComponent bir metin tabanlı form kontrolü etrafında dönüyorsa, bu şöyle görünebilir:

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

ve CustomInputComponent öğesinin HTML'inde:

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

Bunu ayrıca Angular belgelerinde açıklandığı gibi doğrudan giriş öğesine de yazabilirsiniz.

Şimdi bileşeninizin içinde, dışarıda bir şey değiştiğinde neler olduğunu ele aldınız. Şimdi diğer yöne bakalım. Bileşeninizin içinde bir şey değiştiğinde dış dünyayı nasıl bilgilendirirsiniz?

onChange'i çağırmak

Sonraki adım, ebeveyn bileşenini CustomInputComponent içindeki değişiklikler hakkında bilgilendirmektir. İşte yukarıdaki (c) bölümünden onChange ve onTouch fonksiyonlarının devreye girdiği yer burasıdır. Bu fonksiyonları çağırarak, bileşeniniz içindeki değişiklikler hakkında dışarıyı bilgilendirebilirsiniz. Değer değişikliklerini dışarıya iletmek için onChange'i yeni değerle argüman olarak çağırmanız gerekir. Örneğin, kullanıcı özel bileşeninizdeki input alanına bir şeyler yazarsa, güncellenmiş değer ile onChange çağırırsınız:

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

Yukarıdaki (c) uygulamayı tekrar kontrol ederseniz, neler olduğunu göreceksiniz: Angular kendi uygulamasını onChange sınıf özelliğine bağladı. Bu uygulama bir argüman bekliyor, bu da güncellenmiş kontrol değeri. Şu anda yaptığınız şey, o yöntemi çağırmak ve böylece Angular'a değişikliği bildirmek. Angular şimdi dışarıdaki form değerini değiştirecek. Bu işin anahtarı bu. Angular'a form kontrolünü ne zaman ve hangi değerle güncellemesi gerektiğini onChange'i çağırarak söylediniz. Ona "kontrol değerine erişme" imkânını vermiş oldunuz.

Bu arada: onChange adı benim tarafımdan seçildi. Burada istediğinizi seçebilirsiniz, örneğin propagateChange veya benzeri. Ancak nasıl adlandırırsanız adlandırın, Angular tarafından sağlanan ve çalışma zamanında registerOnChange yöntemi ile sınıfınıza bağlanan tek bir argüman alan aynı işlev olacaktır.

onTouch'u çağırmak

Form kontrolleri “dokunulmuş” olabileceğinden, özel form kontrolünüzün ne zaman dokunulduğunu Angular’ın anlaması için de bir yöntem sağlamalısınız. Tahmin ettiğiniz gibi, bunu onTouch fonksiyonunu çağırarak yapabilirsiniz. Örnek olarak burada, Angular’ın hazır form kontrolleri için yaptığı şekilde uyumlu kalmak istiyorsanız, giriş alanı odaktan çıkınca onTouch fonksiyonunu çağırmalısınız:

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

Yine, onTouch benim seçtiğim bir isimdir, ancak gerçek işlevi Angular tarafından sağlanır ve hiçbir argüman almaz. Bu mantıklıdır, çünkü sadece Angular'a form kontrolünün dokunulduğunu bildiriyorsunuz.

Hepsini bir araya getirmek

Peki tüm bunlar bir araya geldiğinde nasıl görünüyor? Şöyle görünmelidir:

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

Daha Fazla Örnek

  • Girdi ile Örnek: https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
  • Tembel Yüklenen Giriş ile Örnek: https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
  • Düğmeli Örnek: https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean

İç İçe Formlar

Kontrol Değeri Erişimcilerinin (Control Value Accessors) iç içe geçmiş form grupları için doğru araç olmadığını unutmayın. İç içe geçmiş form grupları için bunun yerine basitçe bir @Input() subform kullanabilirsiniz. Kontrol Değeri Erişimcileri kontrolleri sarmak için tasarlanmıştır, grupları değil! İç içe geçmiş bir form için bir input nasıl kullanılır örneğini buradan görebilirsiniz: https://stackblitz.com/edit/angular-nested-forms-input-2

Kaynaklar

  • https://angular.io/api/forms/ControlValueAccessor
  • https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/

İlgili makaleler