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 您可以通过执行以下操作重现问题:

  1. 点击 Inactive 单选按钮(应正确显示蓝色状态)
  2. 点击重置按钮(两个收音机都为空,但您会看到表格正确显示活动)

我认为问题在于您只是在勾选复选框时进行交流...所以您的 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
}

Forked Stackblitz

这确实令人困惑,但答案在这里。您不应在单选按钮控件内处理 ngModelngModelChange

要使其正常工作,需要做几件事:

  1. 每当外部更改值时,将内部单选按钮的 checked 属性 设置为正确的值。这是在 writeChanges.

    中完成的
  2. 每当值从内部发生变化(通过用户单击)时,通知外界它已发生变化。这是通过监听 change 并调用 onChange.

    来完成的
  3. 在初始化时,确保checked 属性是根据值设置的。

这是您的 stackblitz,已清理并修复。

总而言之,这就是我在模板中所做的:

  • 从内部单选按钮中删除了 [(ngModel)](ngModelChange) 绑定。
  • 向单选按钮添加 (change)="onInputChange($event)

在class:

  • onInputChange 修改为仅调用 this.onChange(this.valueName) - 这会将更改通知外界,无需执行任何其他操作。
  • 修改writeValue更新内部单选按钮的选中状态(使用ViewChild),这是向内的方向,只有内部单选按钮需要改变,其他没有。

注意:由于在第一次调用 writeValue 时未初始化单选按钮视图子项,因此我还实现了 AfterViewInit 并在那里更新了 checked 属性出色地。查看 stackblitz 以获取说明。

问题

根据您的行 constructor( private _cd: ChangeDetectorRef) { },很明显您了解实际问题所在,未在自定义组件上触发更改检测。

只有一点需要注意,考虑下面的流程

  1. 窗体控件触发检测
  <MyForm>
   <RadioGroup>
     <Radio></Radio>
     <Radio></Radio> <!-- Trigger change detection here -->
     <Radio></Radio>
   <RadioGroup>
  </MyForm>

  1. 变化向上传播
  <MyForm>
   <RadioGroup> <!-- Trigger change detection here -->
     <Radio></Radio>
     <Radio></Radio> 
     <Radio></Radio>
   <RadioGroup>
  </MyForm>

  1. 变化向上传播
  <MyForm> <!-- Trigger change detection here -->
   <RadioGroup> 
     <Radio></Radio>
     <Radio></Radio> 
     <Radio></Radio>
   <RadioGroup>
  </MyForm>

  1. 总体而言,以下组件未检测到此更改
  <MyForm> 
   <RadioGroup> 
     <Radio></Radio><!-- No Change detection here -->
     <Radio></Radio> 
     <Radio></Radio><!-- No Change detection here -->
   <RadioGroup>
  </MyForm>

所以基本上其他表单控件不会更新,因为它们不知道新的更改

解决方案

解决方案是强制更改父项中的值。这样所有的子输入都会更新

一个简单的解决方案是简单地观察 valueChanges 并更新控制值

在您的 AppComponentngOnInit 中添加以下行

    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