Angular4 - Không có giá trị truy cập để kiểm soát biểu mẫu


146

Tôi có một yếu tố tùy chỉnh:

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

Khi tôi cố gắng thêm formControlName, tôi nhận được thông báo lỗi:

Lỗi LRI: Không có trình truy cập giá trị cho điều khiển biểu mẫu có tên: 'SurveyType'

Tôi đã cố gắng để thêm ngDefaultControlmà không thành công. Có vẻ như là vì không có đầu vào / chọn ... và tôi không biết phải làm gì.

Tôi muốn liên kết nhấp chuột của mình vào formControl này để khi ai đó nhấp vào toàn bộ thẻ sẽ đẩy 'loại' của tôi vào formControl. Có thể không?


Tôi không biết quan điểm của mình là: formControl dùng để điều khiển biểu mẫu trong html nhưng div không phải là điều khiển biểu mẫu. Tôi muốn tu liên kết khảo sát của tôi với type.id của thẻ div của tôi
jbtd

Tôi biết tôi có thể sử dụng cách thức góc cạnh cũ và có selectType liên kết với nó nhưng tôi đã cố gắng sử dụng và tìm hiểu hình thức phản ứng từ góc 4 và không biết cách sử dụng formControl với loại trường hợp này.
jbtd

Ok tôi có thể jsut rằng trường hợp đó không thể được xử lý bằng một hình thức phản ứng như vậy. Thx anyway :)
jbtd

Tôi đã đưa ra câu trả lời về cách chia nhỏ các biểu mẫu lớn thành các thành phần phụ ở đây stackoverflow.com/a/56375605/2398593 nhưng điều này cũng áp dụng rất tốt chỉ với một trình truy cập giá trị điều khiển tùy chỉnh. Ngoài ra hãy xem github.com/cloudnc/ngx-sub-form :)
maxime1992

Câu trả lời:


250

Bạn chỉ có thể sử dụng formControlNametrên các chỉ thị thực hiện ControlValueAccessor.

Thực hiện giao diện

Vì vậy, để làm những gì bạn muốn, bạn phải tạo một thành phần thực hiện ControlValueAccessor, có nghĩa là thực hiện ba chức năng sau:

  • writeValue (nói với Angular cách ghi giá trị từ mô hình vào dạng xem)
  • registerOnChange (đăng ký một hàm xử lý được gọi khi khung nhìn thay đổi)
  • registerOnTouched (đăng ký một trình xử lý được gọi khi thành phần nhận được một sự kiện chạm, hữu ích để biết liệu thành phần đó có được tập trung không).

Đăng ký một nhà cung cấp

Sau đó, bạn phải nói với Angular rằng lệnh này là ControlValueAccessor(giao diện sẽ không bị cắt vì nó bị tước khỏi mã khi TypeScript được biên dịch thành JavaScript). Bạn làm điều này bằng cách đăng ký một nhà cung cấp .

Nhà cung cấp nên cung cấp NG_VALUE_ACCESSORsử dụng một giá trị hiện có . Bạn cũng sẽ cần một forwardRefở đây. Lưu ý rằng NG_VALUE_ACCESSORnên là một nhà cung cấp đa .

Ví dụ: nếu chỉ thị tùy chỉnh của bạn được đặt tên MyControlComponent, bạn nên thêm một cái gì đó dọc theo các dòng sau bên trong đối tượng được chuyển đến @Componenttrang trí:

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

Sử dụng

Thành phần của bạn đã sẵn sàng để được sử dụng. Với các biểu mẫu hướng mẫu , ngModelràng buộc bây giờ sẽ hoạt động đúng.

Với các hình thức phản ứng , bây giờ bạn có thể sử dụng đúng cách formControlNamevà điều khiển biểu mẫu sẽ hoạt động như mong đợi.

Tài nguyên


72

Tôi nghĩ bạn nên sử dụng formControlName="surveyType"trên inputvà khôngdiv


Có chắc chắn, nhưng tôi không biết làm thế nào để biến div thẻ của mình thành một thứ khác sẽ là điều khiển dạng html
jbtd

5
Quan điểm của CustomValueAccessor là thêm điều khiển biểu mẫu vào BẤT CỨ
THỨ NÀO

4
@SoEzPz Đây là một mẫu xấu. Bạn bắt chước chức năng Nhập trong một thành phần trình bao bọc, tự thực hiện lại các phương thức HTML tiêu chuẩn (do đó về cơ bản là phát minh lại bánh xe và làm cho mã của bạn dài dòng). nhưng trong 90% trường hợp bạn có thể thực hiện tất cả những gì bạn muốn bằng cách sử dụng <ng-content>trong thành phần trình bao bọc và để thành phần cha mẹ xác định formControlsđơn giản là đặt <input> bên trong <Wrapper>
Phil

3

Lỗi có nghĩa là Angular không biết phải làm gì khi bạn đặt formControla div. Để khắc phục điều này, bạn có hai lựa chọn.

  1. Bạn đặt formControlNamephần tử trên, được Angular hỗ trợ ra khỏi hộp. Đó là: input, textareaselect.
  2. Bạn thực hiện ControlValueAccessorgiao diện. Bằng cách làm như vậy, bạn đang nói với Angular "cách truy cập giá trị của quyền kiểm soát của bạn" (do đó là tên). Hoặc nói một cách đơn giản: Phải làm gì, khi bạn đặt một formControlNamephần tử, điều đó không tự nhiên có giá trị liên quan đến nó.

Bây giờ, việc thực hiện ControlValueAccessorgiao diện có thể hơi khó khăn lúc đầu. Đặc biệt là vì không có nhiều tài liệu tốt về điều này ngoài kia và bạn cần thêm rất nhiều bản tóm tắt vào mã của mình. Vì vậy, hãy để tôi thử phá vỡ điều này trong một số bước đơn giản để làm theo.

Di chuyển điều khiển biểu mẫu của bạn vào thành phần của chính nó

Để thực hiện ControlValueAccessor, bạn cần tạo một thành phần mới (hoặc chỉ thị). Di chuyển mã liên quan đến điều khiển biểu mẫu của bạn ở đó. Như thế này nó cũng sẽ dễ dàng tái sử dụng. Có một điều khiển đã có trong một thành phần có thể là lý do ngay từ đầu, tại sao bạn cần triển khai ControlValueAccessorgiao diện, vì nếu không, bạn sẽ không thể sử dụng thành phần tùy chỉnh của mình cùng với các hình thức Angular.

Thêm bản tóm tắt vào mã của bạn

Việc triển khai ControlValueAccessorgiao diện khá dài dòng, đây là bản tóm tắt đi kèm:

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
  }

Vì vậy, các bộ phận cá nhân đang làm gì?

  • a) Cho phép Angular biết trong thời gian chạy mà bạn đã triển khai ControlValueAccessorgiao diện
  • b) Đảm bảo rằng bạn đang thực hiện ControlValueAccessorgiao diện
  • c) Đây có lẽ là phần khó hiểu nhất. Về cơ bản những gì bạn đang làm là, bạn cung cấp cho Angular phương tiện để ghi đè các thuộc tính / phương thức lớp của bạn onChangeonTouchvới việc thực hiện chính nó trong thời gian chạy, để sau đó bạn có thể gọi các hàm đó. Vì vậy, điểm này rất quan trọng để hiểu: Bạn không cần phải thực hiện onChange và onTouch cho mình (khác với triển khai trống ban đầu). Điều duy nhất bạn làm với (c) là để Angular gắn các chức năng riêng của nó vào lớp của bạn. Tại sao? Vì vậy, bạn có thể gọi các onChangeonTouch phương pháp được cung cấp bởi góc tại thời điểm thích hợp. Chúng ta sẽ thấy cách thức hoạt động dưới đây.
  • d) Chúng ta cũng sẽ thấy writeValuephương thức hoạt động trong phần tiếp theo, khi chúng ta thực hiện nó. Tôi đã đặt nó ở đây, vì vậy tất cả các thuộc tính bắt buộc ControlValueAccessorđược triển khai và mã của bạn vẫn biên dịch.

Thực hiện writeValue

Điều gì writeValuelàm, là làm một cái gì đó bên trong thành phần tùy chỉnh của bạn, khi điều khiển biểu mẫu được thay đổi ở bên ngoài . Vì vậy, ví dụ, nếu bạn đã đặt tên cho thành phần điều khiển biểu mẫu tùy chỉnh của mình app-custom-inputvà bạn sẽ sử dụng nó trong thành phần chính như thế này:

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

sau đó writeValueđược kích hoạt bất cứ khi nào thành phần cha mẹ bằng cách nào đó thay đổi giá trị của myFormControl. Điều này có thể là ví dụ trong quá trình khởi tạo biểu mẫu ( this.form = this.formBuilder.group({myFormControl: ""});) hoặc trên thiết lập lại biểu mẫu this.form.reset();.

Những gì bạn thường muốn làm nếu giá trị của điều khiển biểu mẫu thay đổi ở bên ngoài, là ghi nó vào một biến cục bộ đại diện cho giá trị điều khiển biểu mẫu. Ví dụ: nếu bạn CustomInputComponentxoay quanh điều khiển biểu mẫu dựa trên văn bản, nó có thể trông như thế này:

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

và trong html của CustomInputComponent:

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

Bạn cũng có thể viết nó trực tiếp vào phần tử đầu vào như được mô tả trong các tài liệu Angular.

Bây giờ bạn đã xử lý những gì xảy ra bên trong thành phần của bạn khi có gì đó thay đổi bên ngoài. Bây giờ hãy nhìn theo hướng khác. Làm thế nào để bạn thông báo cho thế giới bên ngoài khi một cái gì đó thay đổi bên trong thành phần của bạn?

Gọi điện thoại

Bước tiếp theo là thông báo cho thành phần chính về những thay đổi bên trong của bạn CustomInputComponent . Đây là nơi onChangevà các onTouchchức năng từ (c) từ trên phát huy tác dụng. Bằng cách gọi các chức năng đó, bạn có thể thông báo cho bên ngoài về những thay đổi bên trong thành phần của mình. Để truyền các thay đổi của giá trị ra bên ngoài, bạn cần gọi onChange với giá trị mới làm đối số . Ví dụ: nếu người dùng nhập nội dung nào đó vào inputtrường trong thành phần tùy chỉnh của bạn, bạn gọi onChangevới giá trị được cập nhật:

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

Nếu bạn kiểm tra lại việc thực hiện (c) từ phía trên một lần nữa, bạn sẽ thấy điều gì đang xảy ra: Angular ràng buộc việc triển khai riêng của nó đối với thuộc tính onChangelớp. Việc thực hiện đó mong đợi một đối số, đó là giá trị kiểm soát được cập nhật. Những gì bạn đang làm bây giờ là bạn đang gọi phương thức đó và do đó cho Angular biết về sự thay đổi. Angular bây giờ sẽ đi trước và thay đổi giá trị hình thức ở bên ngoài. Đây là phần quan trọng trong tất cả điều này. Bạn đã nói với Angular khi nào nên cập nhật điều khiển biểu mẫu và với giá trị nào bằng cách gọionChange . Bạn đã cho nó phương tiện để "truy cập giá trị kiểm soát".

Nhân tiện: Tên onChangeđược chọn bởi tôi. Bạn có thể chọn bất cứ điều gì ở đây, ví dụ propagateChangehoặc tương tự. Tuy nhiên, bạn đặt tên cho nó, nó sẽ là cùng một hàm có một đối số, được cung cấp bởi Angular và được ràng buộc với lớp của bạn bằng registerOnChangephương thức trong thời gian chạy.

Gọi onTouch

Vì các điều khiển biểu mẫu có thể được "chạm", bạn cũng nên cung cấp cho Angular phương tiện để hiểu khi điều khiển biểu mẫu tùy chỉnh của bạn được chạm vào. Bạn có thể làm điều đó, bạn đoán nó, bằng cách gọi onTouchhàm. Vì vậy, với ví dụ của chúng tôi ở đây, nếu bạn muốn tuân thủ cách Angular thực hiện nó cho các điều khiển biểu mẫu bên ngoài, bạn nên gọionTouch khi trường đầu vào bị mờ:

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

Một lần nữa, onTouchlà tên do tôi chọn, nhưng chức năng thực tế của nó được cung cấp bởi Angular và nó không có đối số. Điều này có ý nghĩa, vì bạn chỉ cần cho Angular biết, rằng điều khiển biểu mẫu đã được chạm vào.

Để tất cả chúng cùng nhau

Vì vậy, làm thế nào mà nhìn khi tất cả cùng nhau? Nó sẽ giống như thế này:

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

Thêm ví dụ

Các hình thức lồng nhau

Lưu ý rằng Access Access Control KHÔNG phải là công cụ phù hợp cho các nhóm biểu mẫu lồng nhau. Đối với các nhóm biểu mẫu lồng nhau, bạn chỉ cần sử dụng một @Input() subformthay thế. Kiểm soát giá trị truy cập có nghĩa là để bọc controls, không groups! Xem ví dụ này làm thế nào để sử dụng đầu vào cho một hình thức lồng nhau: https://stackblitz.com/edit/angular-nested-forms-input-2

Nguồn


-1

Đối với tôi, đó là do thuộc tính "nhiều" trên điều khiển đầu vào được chọn vì Angular có ValueAccessor khác nhau cho loại điều khiển này.

const countryControl = new FormControl();

Và bên trong mẫu sử dụng như thế này

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

Thêm chi tiết tham khảo Tài liệu chính thức


Điều gì là do "nhiều"? Tôi không thấy cách mã của bạn giải quyết bất cứ điều gì, hoặc vấn đề ban đầu là gì. Mã của bạn cho thấy việc sử dụng cơ bản thông thường.
Lazar Ljubenović
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.