Angular 自定义单选组件 2 路绑定
Angular Custom Radio Component 2-way Binding
我创建了一个自定义单选组件,它只是将我们的单选按钮的样式更改为在其中包含复选标记。我实现了 ControlValueAccessor
以便我可以将元素与 Reactive Forms 一起使用,并且当您单击 UI 中的单选按钮时组件可以正常工作。我遇到的问题是,当我尝试从我的组件设置值而不是通过 UI 交互(特别是尝试重置表单)时,值在反应形式上正确更改,但 UI未更新。
这是我的自定义无线电控件:
import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'k-checkmark-radio',
templateUrl: './checkmark-radio.component.html',
styleUrls: ['./checkmark-radio.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckmarkRadioComponent),
multi: true
}]
})
export class CheckmarkRadioComponent implements OnInit, ControlValueAccessor {
@Input() groupName: string = "";
@Input() label: string = "";
@Input() valueName: string = "";
@Input() radioId!: string;
public checked: boolean;
public value!: string;
constructor( private _cd: ChangeDetectorRef) { }
onChange: any = () => {}
onTouch: any = () => {}
onInputChange(val: string){
this.checked = val == this.valueName;
this.onChange(val);
}
writeValue(value: string): void {
this.value = value;
this.checked = value == this.valueName;
console.log(`${this.valueName} Writing Checkmark Value: `, value, this.checked);
this._cd.markForCheck();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
ngOnInit(): void {
}
}
这是它的模板:
<div class="form-check form-check-inline">
<label>
<input
type="radio"
[id]="groupName + '_' + valueName"
(ngModelChange)="onInputChange($event)"
[name]="groupName"
[value]="valueName"
[(ngModel)]="value">
<span class="label-size">{{ label }}</span>
</label>
</div>
<br /> Checked? {{ checked }}
我在这里设置了一个问题的工作示例:https://stackblitz.com/edit/angular-ivy-23crge?file=src/app/checkmark-radio/checkmark-radio.component.html
您可以通过执行以下操作重现问题:
- 点击 Inactive 单选按钮(应正确显示蓝色状态)
- 点击重置按钮(两个收音机都为空,但您会看到表格正确显示活动)
我认为问题在于您只是在勾选复选框时进行交流...所以您的 children 每次都与 parent 交谈,但 parent 不会与其他人交流 child 发生了什么。您可以将当前状态传达给 children。
但这里有一个快速而肮脏的:
通过在点击时使用您的函数,您可以确保更新活动值和非活动值:
<k-checkmark-radio
formControlName="status"
groupName="status"
valueName="Active"
class="mr-3"
label="Active"
(click)="makeActive()">
</k-checkmark-radio>
<k-checkmark-radio
formControlName="status"
groupName="status"
valueName="Inactive"
label="Inactive"
(click)="makeInactive()">
</k-checkmark-radio>
明天我会尝试得到一个更漂亮的解决方案,但同时这可能会有所帮助。
这里的问题是,在反应形式的情况下 Angular 不会在控件之间同步视图。对于模板驱动的表单,它将自动完成,因为 ngOnChanges
.
您可以通过在 onChange
调用之后对基础 FormControl
调用 setValue/patchValue
来强制同步。
区别在于 onChange
不同步视图
control.setValue(control._pendingValue, { emitModelToViewChange: false });
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为了在 ControlValueAccessor
中访问 FormControl
你可以使用 Angular Injector
:
复选标记-radio.component.ts
import { NgControl } from '@angular/forms';
...
constructor(private inj: Injector, ...) { }
onInputChange(val: any){
...
this.onChange(val);
this.inj.get(NgControl).control.setValue(val); <=== this one
}
这确实令人困惑,但答案在这里。您不应在单选按钮控件内处理 ngModel
或 ngModelChange
。
要使其正常工作,需要做几件事:
每当外部更改值时,将内部单选按钮的 checked
属性 设置为正确的值。这是在 writeChanges
.
中完成的
每当值从内部发生变化(通过用户单击)时,通知外界它已发生变化。这是通过监听 change
并调用 onChange
.
来完成的
在初始化时,确保checked
属性是根据值设置的。
这是您的 stackblitz,已清理并修复。
总而言之,这就是我在模板中所做的:
- 从内部单选按钮中删除了
[(ngModel)]
和 (ngModelChange)
绑定。
- 向单选按钮添加
(change)="onInputChange($event)
在class:
- 将
onInputChange
修改为仅调用 this.onChange(this.valueName)
- 这会将更改通知外界,无需执行任何其他操作。
- 修改
writeValue
更新内部单选按钮的选中状态(使用ViewChild
),这是向内的方向,只有内部单选按钮需要改变,其他没有。
注意:由于在第一次调用 writeValue
时未初始化单选按钮视图子项,因此我还实现了 AfterViewInit
并在那里更新了 checked
属性出色地。查看 stackblitz 以获取说明。
问题
根据您的行 constructor( private _cd: ChangeDetectorRef) { }
,很明显您了解实际问题所在,未在自定义组件上触发更改检测。
只有一点需要注意,考虑下面的流程
- 窗体控件触发检测
<MyForm>
<RadioGroup>
<Radio></Radio>
<Radio></Radio> <!-- Trigger change detection here -->
<Radio></Radio>
<RadioGroup>
</MyForm>
- 变化向上传播
<MyForm>
<RadioGroup> <!-- Trigger change detection here -->
<Radio></Radio>
<Radio></Radio>
<Radio></Radio>
<RadioGroup>
</MyForm>
- 变化向上传播
<MyForm> <!-- Trigger change detection here -->
<RadioGroup>
<Radio></Radio>
<Radio></Radio>
<Radio></Radio>
<RadioGroup>
</MyForm>
- 总体而言,以下组件未检测到此更改
<MyForm>
<RadioGroup>
<Radio></Radio><!-- No Change detection here -->
<Radio></Radio>
<Radio></Radio><!-- No Change detection here -->
<RadioGroup>
</MyForm>
所以基本上其他表单控件不会更新,因为它们不知道新的更改
解决方案
解决方案是强制更改父项中的值。这样所有的子输入都会更新
一个简单的解决方案是简单地观察 valueChanges
并更新控制值
在您的 AppComponent
的 ngOnInit
中添加以下行
this.form.get("status").valueChanges.subscribe({
next: (y) => {
this.form.get("status").setValue(y, {emitEvent: false})
}
})
注意:不要忘记emitEvent: false
这避免了Maximum call stack exceeded error
看到这个forked demo
我创建了一个自定义单选组件,它只是将我们的单选按钮的样式更改为在其中包含复选标记。我实现了 ControlValueAccessor
以便我可以将元素与 Reactive Forms 一起使用,并且当您单击 UI 中的单选按钮时组件可以正常工作。我遇到的问题是,当我尝试从我的组件设置值而不是通过 UI 交互(特别是尝试重置表单)时,值在反应形式上正确更改,但 UI未更新。
这是我的自定义无线电控件:
import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'k-checkmark-radio',
templateUrl: './checkmark-radio.component.html',
styleUrls: ['./checkmark-radio.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckmarkRadioComponent),
multi: true
}]
})
export class CheckmarkRadioComponent implements OnInit, ControlValueAccessor {
@Input() groupName: string = "";
@Input() label: string = "";
@Input() valueName: string = "";
@Input() radioId!: string;
public checked: boolean;
public value!: string;
constructor( private _cd: ChangeDetectorRef) { }
onChange: any = () => {}
onTouch: any = () => {}
onInputChange(val: string){
this.checked = val == this.valueName;
this.onChange(val);
}
writeValue(value: string): void {
this.value = value;
this.checked = value == this.valueName;
console.log(`${this.valueName} Writing Checkmark Value: `, value, this.checked);
this._cd.markForCheck();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
ngOnInit(): void {
}
}
这是它的模板:
<div class="form-check form-check-inline">
<label>
<input
type="radio"
[id]="groupName + '_' + valueName"
(ngModelChange)="onInputChange($event)"
[name]="groupName"
[value]="valueName"
[(ngModel)]="value">
<span class="label-size">{{ label }}</span>
</label>
</div>
<br /> Checked? {{ checked }}
我在这里设置了一个问题的工作示例:https://stackblitz.com/edit/angular-ivy-23crge?file=src/app/checkmark-radio/checkmark-radio.component.html 您可以通过执行以下操作重现问题:
- 点击 Inactive 单选按钮(应正确显示蓝色状态)
- 点击重置按钮(两个收音机都为空,但您会看到表格正确显示活动)
我认为问题在于您只是在勾选复选框时进行交流...所以您的 children 每次都与 parent 交谈,但 parent 不会与其他人交流 child 发生了什么。您可以将当前状态传达给 children。
但这里有一个快速而肮脏的:
通过在点击时使用您的函数,您可以确保更新活动值和非活动值:
<k-checkmark-radio
formControlName="status"
groupName="status"
valueName="Active"
class="mr-3"
label="Active"
(click)="makeActive()">
</k-checkmark-radio>
<k-checkmark-radio
formControlName="status"
groupName="status"
valueName="Inactive"
label="Inactive"
(click)="makeInactive()">
</k-checkmark-radio>
明天我会尝试得到一个更漂亮的解决方案,但同时这可能会有所帮助。
这里的问题是,在反应形式的情况下 Angular 不会在控件之间同步视图。对于模板驱动的表单,它将自动完成,因为 ngOnChanges
.
您可以通过在 onChange
调用之后对基础 FormControl
调用 setValue/patchValue
来强制同步。
区别在于 onChange
不同步视图
control.setValue(control._pendingValue, { emitModelToViewChange: false });
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为了在 ControlValueAccessor
中访问 FormControl
你可以使用 Angular Injector
:
复选标记-radio.component.ts
import { NgControl } from '@angular/forms';
...
constructor(private inj: Injector, ...) { }
onInputChange(val: any){
...
this.onChange(val);
this.inj.get(NgControl).control.setValue(val); <=== this one
}
这确实令人困惑,但答案在这里。您不应在单选按钮控件内处理 ngModel
或 ngModelChange
。
要使其正常工作,需要做几件事:
每当外部更改值时,将内部单选按钮的
中完成的checked
属性 设置为正确的值。这是在writeChanges
.每当值从内部发生变化(通过用户单击)时,通知外界它已发生变化。这是通过监听
来完成的change
并调用onChange
.在初始化时,确保
checked
属性是根据值设置的。
这是您的 stackblitz,已清理并修复。
总而言之,这就是我在模板中所做的:
- 从内部单选按钮中删除了
[(ngModel)]
和(ngModelChange)
绑定。 - 向单选按钮添加
(change)="onInputChange($event)
在class:
- 将
onInputChange
修改为仅调用this.onChange(this.valueName)
- 这会将更改通知外界,无需执行任何其他操作。 - 修改
writeValue
更新内部单选按钮的选中状态(使用ViewChild
),这是向内的方向,只有内部单选按钮需要改变,其他没有。
注意:由于在第一次调用 writeValue
时未初始化单选按钮视图子项,因此我还实现了 AfterViewInit
并在那里更新了 checked
属性出色地。查看 stackblitz 以获取说明。
问题
根据您的行 constructor( private _cd: ChangeDetectorRef) { }
,很明显您了解实际问题所在,未在自定义组件上触发更改检测。
只有一点需要注意,考虑下面的流程
- 窗体控件触发检测
<MyForm>
<RadioGroup>
<Radio></Radio>
<Radio></Radio> <!-- Trigger change detection here -->
<Radio></Radio>
<RadioGroup>
</MyForm>
- 变化向上传播
<MyForm>
<RadioGroup> <!-- Trigger change detection here -->
<Radio></Radio>
<Radio></Radio>
<Radio></Radio>
<RadioGroup>
</MyForm>
- 变化向上传播
<MyForm> <!-- Trigger change detection here -->
<RadioGroup>
<Radio></Radio>
<Radio></Radio>
<Radio></Radio>
<RadioGroup>
</MyForm>
- 总体而言,以下组件未检测到此更改
<MyForm>
<RadioGroup>
<Radio></Radio><!-- No Change detection here -->
<Radio></Radio>
<Radio></Radio><!-- No Change detection here -->
<RadioGroup>
</MyForm>
所以基本上其他表单控件不会更新,因为它们不知道新的更改
解决方案
解决方案是强制更改父项中的值。这样所有的子输入都会更新
一个简单的解决方案是简单地观察 valueChanges
并更新控制值
在您的 AppComponent
的 ngOnInit
中添加以下行
this.form.get("status").valueChanges.subscribe({
next: (y) => {
this.form.get("status").setValue(y, {emitEvent: false})
}
})
注意:不要忘记emitEvent: false
这避免了Maximum call stack exceeded error
看到这个forked demo