Angular4 - ไม่มีค่าตัวเข้าถึงสำหรับการควบคุมฟอร์ม


146

ฉันมีองค์ประกอบที่กำหนดเอง:

<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 ฉันได้รับข้อความแสดงข้อผิดพลาด:

ข้อผิดพลาดข้อผิดพลาด: ไม่มีการเข้าถึงค่าสำหรับการควบคุมแบบฟอร์มที่มีชื่อ: 'surveyType'

ฉันพยายามที่จะเพิ่มngDefaultControlไม่ประสบความสำเร็จ ดูเหมือนว่าเป็นเพราะไม่มีอินพุต / เลือก ... และฉันไม่รู้ว่าต้องทำอย่างไร

ฉันต้องการผูกคลิกของฉันไปที่ formControl นี้เพื่อที่เมื่อมีคนคลิกที่การ์ดทั้งหมดที่จะผลักดัน 'ประเภท' ของฉันลงใน formControl เป็นไปได้ไหม?


ฉันไม่ทราบว่าจุดของฉันคือ: formControl ไปสำหรับการควบคุมรูปแบบใน html แต่ div ไม่ใช่การควบคุมรูปแบบ ฉันต้องการ tu ผูกแบบสำรวจของฉันด้วย type.id บัตร div ของฉัน
jbtd

ฉันรู้ว่าฉันสามารถใช้แองกูลาร์แบบเก่าได้และเลือกประเภทของฉันไว้ แต่ฉันพยายามใช้และเรียนรู้รูปแบบปฏิกิริยาจากแองกูลาร์ 4 และไม่รู้วิธีใช้ formControl กับกรณีประเภทนี้
jbtd

ตกลงฉันอาจ jsut กรณีที่ไม่สามารถจัดการโดยแบบฟอร์มปฏิกิริยาดังนั้น ต่อไป :) :)
jbtd

ฉันได้คำตอบเกี่ยวกับวิธีการแบ่งฟอร์มขนาดใหญ่เป็นคอมโพเนนต์ย่อยที่นี่stackoverflow.com/a/56375605/2398593แต่สิ่งนี้ยังใช้ได้ดีกับ accessor ค่าควบคุมที่กำหนดเอง ตรวจสอบgithub.com/cloudnc/ngx-sub-form ด้วย :)
maxime1992

คำตอบ:


251

คุณสามารถformControlNameใช้ได้เฉพาะกับคำสั่งที่ใช้ControlValueAccessorเฉพาะในคำสั่งที่ใช้

ใช้อินเทอร์เฟซ

ดังนั้นเพื่อที่จะทำสิ่งที่คุณต้องการคุณต้องสร้างส่วนประกอบที่ใช้งานControlValueAccessorซึ่งหมายถึงการใช้ฟังก์ชั่นสามอย่างต่อไปนี้ :

  • writeValue (บอกเชิงมุมถึงวิธีการเขียนค่าจากแบบจำลองลงในมุมมอง)
  • registerOnChange (ลงทะเบียนฟังก์ชันตัวจัดการที่ถูกเรียกเมื่อมุมมองเปลี่ยนไป)
  • registerOnTouched (ลงทะเบียนตัวจัดการที่จะถูกเรียกเมื่อส่วนประกอบได้รับเหตุการณ์การสัมผัสซึ่งมีประโยชน์สำหรับการรู้ว่าองค์ประกอบได้รับการโฟกัสแล้ว)

ลงทะเบียนผู้ให้บริการ

จากนั้นคุณต้องบอก Angular ว่าคำสั่งนี้เป็นControlValueAccessor(ส่วนต่อประสานจะไม่ตัดมันเนื่องจากมันจะถูกตัดออกจากโค้ดเมื่อ TypeScript ถูกคอมไพล์เป็น JavaScript) คุณทำได้โดยการลงทะเบียนผู้ให้บริการ

ผู้ให้บริการควรมีNG_VALUE_ACCESSORและใช้ค่าที่มีอยู่ คุณจะต้องมีforwardRefที่นี่ โปรดทราบว่าNG_VALUE_ACCESSORควรจะมีผู้ให้บริการหลาย

ตัวอย่างเช่นหากคำสั่งที่กำหนดเองของคุณชื่อ MyControlComponent คุณควรเพิ่มบางอย่างตามบรรทัดต่อไปนี้ภายในวัตถุที่ส่งไปยัง@Componentมัณฑนากร:

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

การใช้

ส่วนประกอบของคุณพร้อมใช้งานแล้ว ด้วยรูปแบบแม่แบบขับเคลื่อน , ngModelผูกพันในขณะนี้จะทำงานอย่างถูกต้อง

ด้วยฟอร์มที่มีปฏิกิริยาคุณสามารถใช้งานได้อย่างถูกต้องformControlNameและการควบคุมฟอร์มจะทำงานตามที่คาดไว้

ทรัพยากร


72

ฉันคิดว่าคุณควรใช้formControlName="surveyType"กับinputและไม่ใช่div


ใช่แน่นอน แต่ฉันไม่รู้วิธีเปลี่ยนการ์ดของฉันให้เป็นอย่างอื่นซึ่งจะเป็นการควบคุมรูปแบบ html
jbtd

5
จุดประสงค์ของ CustomValueAccessor คือการเพิ่มการควบคุมแบบฟอร์มลงในสิ่งใด ๆ แม้แต่ div
SoEzPz

4
@SoEzPz นี่เป็นรูปแบบที่ไม่ดี คุณเลียนแบบฟังก์ชั่นอินพุตในส่วนประกอบของ wrapper โดยใช้วิธีมาตรฐาน HTML อีกครั้งด้วยตัวคุณเอง แต่ใน 90% ของกรณีที่คุณสามารถทำได้ทั้งหมดที่คุณต้องการโดยใช้<ng-content>ในองค์ประกอบ wrapper และปล่อยให้องค์ประกอบหลักที่กำหนดformControlsเพียงแค่ใส่ <input> ภายใน <wrapper>
Phil

3

หมายถึงข้อผิดพลาดที่เชิงมุมไม่ทราบว่าจะทำอย่างไรเมื่อคุณใส่ในformControl divในการแก้ไขปัญหานี้คุณมีสองตัวเลือก

  1. คุณใส่formControlNameองค์ประกอบบนซึ่งได้รับการสนับสนุนโดย Angular ออกจากกล่อง เหล่านี้คือ: input, และtextareaselect
  2. คุณใช้ControlValueAccessorอินเทอร์เฟซ ด้วยการทำเช่นนี้คุณกำลังบอก Angular "วิธีเข้าถึงคุณค่าของการควบคุมของคุณ" (ชื่อจึงเป็นเช่นนั้น) หรือพูดง่ายๆ: สิ่งที่ต้องทำเมื่อคุณใส่formControlNameองค์ประกอบที่ไม่มีคุณค่าเกี่ยวข้องกับมันตามธรรมชาติ

ตอนนี้การใช้ControlValueAccessorอินเทอร์เฟซอาจเป็นบิตที่น่ากลัวในตอนแรก โดยเฉพาะอย่างยิ่งเนื่องจากไม่มีเอกสารที่ดีเกี่ยวกับเรื่องนี้และคุณต้องเพิ่มจำนวนมากในรหัสของคุณ ดังนั้นให้ฉันลองทำตามขั้นตอนง่าย ๆ

ย้ายการควบคุมแบบฟอร์มของคุณไปยังองค์ประกอบของตัวเอง

เพื่อที่จะใช้งาน ControlValueAccessorคุณจะต้องสร้างองค์ประกอบใหม่ (หรือคำสั่ง) ย้ายรหัสที่เกี่ยวข้องกับการควบคุมแบบฟอร์มของคุณไปที่นั่น เช่นนี้มันจะสามารถนำมาใช้ใหม่ได้อย่างง่ายดาย การมีการควบคุมอยู่ในองค์ประกอบอยู่แล้วอาจเป็นเหตุผลแรกที่คุณต้องใช้ControlValueAccessorอินเทอร์เฟซเพราะไม่เช่นนั้นคุณจะไม่สามารถใช้องค์ประกอบที่กำหนดเองของคุณพร้อมกับฟอร์ม Angular

เพิ่มสำเร็จรูปลงในรหัสของคุณ

การใช้ControlValueAccessorอินเทอร์เฟซค่อนข้างละเอียดนี่คือต้นแบบที่มาพร้อมกับมัน:

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
  }

ดังนั้นแต่ละส่วนทำอะไร

  • a) ให้ Angular ทราบระหว่างรันไทม์ที่คุณใช้ ControlValueAccessorอินเตอร์เฟส
  • b) ทำให้แน่ใจว่าคุณกำลังใช้งาน ControlValueAccessorอินเทอร์เฟซ
  • c) นี่อาจเป็นส่วนที่สับสนที่สุด โดยทั่วไปสิ่งที่คุณกำลังทำคือคุณให้ Angular มีความหมายในการแทนที่คุณสมบัติ / วิธีการเรียนของคุณonChangeและonTouchด้วยการใช้งานของมันเองในช่วงรันไทม์ดังนั้นคุณจึงสามารถเรียกใช้ฟังก์ชันเหล่านั้นได้ ดังนั้นประเด็นนี้มีความสำคัญที่จะเข้าใจ: คุณไม่จำเป็นต้องใช้ onChange และ onTouch ด้วยตัวคุณเอง (นอกเหนือจากการใช้งานเริ่มต้นที่ว่างเปล่า) สิ่งเดียวที่คุณทำกับ (c) คือให้ Angular แนบฟังก์ชันของตัวเองกับคลาสของคุณ ทำไม? ดังนั้นคุณก็จะสามารถโทรonChangeและonTouchวิธีการให้บริการโดยเชิงมุมในเวลาที่เหมาะสม เราจะเห็นวิธีการทำงานด้านล่าง
  • d) นอกจากนี้เรายังจะเห็นวิธีwriteValueการทำงานของวิธีการในส่วนถัดไปเมื่อเราใช้งาน ฉันใส่มันไว้ที่นี่คุณสมบัติที่จำเป็นทั้งหมดControlValueAccessorจะถูกนำไปใช้งานและโค้ดของคุณยังคงรวบรวม

ใช้ writeValue

อะไรwriteValueไม่คือการทำสิ่งที่อยู่ภายในองค์ประกอบที่กำหนดเองของคุณเมื่อมีการควบคุมรูปแบบที่มีการเปลี่ยนแปลงที่อยู่ข้างนอก ตัวอย่างเช่นหากคุณตั้งชื่อองค์ประกอบการควบคุมรูปแบบที่กำหนดเองapp-custom-inputและคุณจะใช้มันในองค์ประกอบหลักเช่นนี้:

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

จากนั้นได้รับการเรียกองค์ประกอบเมื่อใดก็ตามที่ผู้ปกครองอย่างใดเปลี่ยนค่าของwriteValue myFormControlนี่อาจเป็นตัวอย่างในระหว่างการเริ่มต้นของแบบฟอร์ม (this.form = this.formBuilder.group({myFormControl: ""}); ) this.form.reset();หรือในการตั้งค่ารูปแบบ

สิ่งที่คุณมักจะต้องการทำถ้าค่าของการควบคุมการเปลี่ยนแปลงรูปแบบด้านนอกคือการเขียนให้ตัวแปรท้องถิ่นซึ่งแสดงถึงค่าการควบคุมแบบฟอร์ม ตัวอย่างเช่นหากคุณCustomInputComponentหมุนรอบตัวควบคุมแบบข้อความข้อความอาจมีลักษณะเช่นนี้:

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

และใน html ของCustomInputComponent:

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

คุณสามารถเขียนโดยตรงไปยังองค์ประกอบอินพุตตามที่อธิบายไว้ในเอกสารเชิงมุม

ตอนนี้คุณได้จัดการสิ่งที่เกิดขึ้นภายในองค์ประกอบของคุณเมื่อมีอะไรเปลี่ยนแปลงภายนอก ทีนี้ลองดูที่ทิศทางอื่น คุณจะแจ้งโลกภายนอกได้อย่างไรเมื่อมีบางสิ่งเปลี่ยนแปลงภายในองค์ประกอบของคุณ

โทรหาที่เปลี่ยน

CustomInputComponentขั้นตอนต่อไปคือการแจ้งให้องค์ประกอบของผู้ปกครองเกี่ยวกับการเปลี่ยนแปลงภายในของคุณ นี่คือที่onChangeและonTouchฟังก์ชั่นจาก (c) จากด้านบนเข้ามาเล่น โดยการเรียกฟังก์ชั่นเหล่านั้นคุณสามารถแจ้งภายนอกเกี่ยวกับการเปลี่ยนแปลงภายในส่วนประกอบของคุณ เพื่อเผยแพร่การเปลี่ยนแปลงของค่าที่ออกไปข้างนอกคุณจะต้องเรียก onChange ด้วยค่าใหม่เป็นอาร์กิวเมนต์ ตัวอย่างเช่นหากผู้ใช้พิมพ์บางอย่างในinputฟิลด์ในคอมโพเนนต์ที่กำหนดเองของคุณคุณจะโทรหาonChangeด้วยค่าที่อัปเดต:

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

หากคุณตรวจสอบการใช้งาน (c) จากด้านบนอีกครั้งคุณจะเห็นสิ่งที่เกิดขึ้น: เชิงมุมผูกติดกับการใช้งานของonChangeคุณสมบัติคลาส การใช้งานนั้นคาดว่าจะมีหนึ่งอาร์กิวเมนต์ซึ่งเป็นค่าควบคุมที่อัปเดต สิ่งที่คุณกำลังทำอยู่ตอนนี้คือคุณกำลังเรียกวิธีการนั้นและแจ้งให้ Angular ทราบเกี่ยวกับการเปลี่ยนแปลง ตอนนี้ Angular จะดำเนินต่อไปและเปลี่ยนค่าของฟอร์มด้านนอก นี่คือส่วนสำคัญในทั้งหมดนี้ คุณบอก Angular เมื่อมันควรจะปรับปรุงการควบคุมแบบฟอร์มและสิ่งที่มีค่าโดยการโทรonChangeคุณบอกเชิงมุมเมื่อมันควรปรับปรุงการควบคุมรูปแบบและมีสิ่งที่มีค่าโดยการโทรคุณได้ให้วิธีการ "เข้าถึงค่าควบคุม"

โดยวิธีการ: ชื่อonChangeถูกเลือกโดยฉัน คุณสามารถเลือกอะไรก็ได้ที่นี่เช่นpropagateChangeหรือคล้ายกัน อย่างไรก็ตามคุณตั้งชื่อมันว่ามันจะเป็นฟังก์ชั่นเดียวกับที่รับหนึ่งอาร์กิวเมนต์ที่จัดทำโดย Angular และที่ถูกผูกไว้กับคลาสของคุณด้วยregisterOnChangeวิธีการระหว่างรันไทม์

โทรบนทัช

เนื่องจากการควบคุมแบบฟอร์มสามารถ "สัมผัส" ได้คุณจึงควรให้ Angular มีวิธีการที่จะเข้าใจเมื่อการควบคุมฟอร์มที่กำหนดเองของคุณถูกแตะ คุณสามารถทำได้คุณเดาได้โดยการเรียกonTouchฟังก์ชั่น ดังนั้นสำหรับตัวอย่างของเราที่นี่หากคุณต้องการให้สอดคล้องกับวิธีการทำ Angular สำหรับการควบคุมแบบฟอร์มนอกกล่องคุณควรเรียกonTouchเมื่อช่องสัญญาณเบลอ:

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

อีกครั้ง onTouchเป็นชื่อที่ฉันเลือก แต่สิ่งที่จริง ๆ แล้วมันคือฟังก์ชั่นที่จัดทำโดย Angular และใช้ศูนย์อาร์กิวเมนต์ ซึ่งเหมาะสมแล้วเนื่องจากคุณเพียงแจ้งให้ Angular ทราบว่าการควบคุมฟอร์มได้รับการสัมผัสแล้ว

วางมันทั้งหมดเข้าด้วยกัน

แล้วมันจะดูอย่างไรเมื่อมันมารวมกัน? ควรมีลักษณะเช่นนี้:

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

ตัวอย่างเพิ่มเติม

แบบฟอร์มซ้อน

โปรดทราบว่าการควบคุมค่า Accessors ไม่ใช่เครื่องมือที่เหมาะสมสำหรับกลุ่มฟอร์มที่ซ้อนกัน สำหรับกลุ่มฟอร์มที่ซ้อนกันคุณสามารถใช้@Input() subformแทนได้ ค่าการควบคุม Accessorsors มีไว้เพื่อห่อcontrolsไม่ใช่groups! ดูตัวอย่างนี้วิธีใช้อินพุตสำหรับฟอร์มซ้อน: https://stackblitz.com/edit/angular-nested-forms-input-2

แหล่งที่มา


-1

สำหรับฉันมันเป็นเพราะแอตทริบิวต์ "หลายคน" ในการควบคุมอินพุตที่เลือกเนื่องจาก Angular มี ValueAccessor ที่แตกต่างกันสำหรับการควบคุมประเภทนี้

const countryControl = new FormControl();

และภายในเทมเพลตก็ใช้สิ่งนี้

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

รายละเอียดเพิ่มเติมอ้างอิงเอกสารอย่างเป็นทางการ


เกิดอะไรขึ้นกับ "หลาย"? ฉันไม่เห็นว่ารหัสของคุณแก้ปัญหาอะไรหรือปัญหาเดิมเป็นอย่างไร รหัสของคุณแสดงการใช้งานพื้นฐานตามปกติ
Lazar Ljubenović
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.