Lỗi có nghĩa là Angular không biết phải làm gì khi bạn đặt formControl
a div
. Để khắc phục điều này, bạn có hai lựa chọn.
- Bạn đặt
formControlName
phần tử trên, được Angular hỗ trợ ra khỏi hộp. Đó là: input
, textarea
và select
.
- Bạn thực hiện
ControlValueAccessor
giao 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 formControlName
phầ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 ControlValueAccessor
giao 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 ControlValueAccessor
giao 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 ControlValueAccessor
giao 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
ControlValueAccessor
giao diện
- b) Đảm bảo rằng bạn đang thực hiện
ControlValueAccessor
giao 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
onChange
và onTouch
vớ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 onChange
vàonTouch
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
writeValue
phươ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ì writeValue
là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-input
và 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 CustomInputComponent
xoay 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 onChange
và các onTouch
chứ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 input
trường trong thành phần tùy chỉnh của bạn, bạn gọi onChange
vớ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 onChange
lớ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ụ propagateChange
hoặ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 registerOnChange
phươ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 onTouch
hà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, onTouch
là 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() subform
thay 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