You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 12 Reactive Forms属性网格的校验与变更检测实现问题

问题1:从FormArray读取类型渲染对应控件

你已经知道formArray.controls是FormGroup数组,只需在模板循环时读取每个FormGroup的value即可拿到存储的type、name等元信息,实现要点:

  • 组件类中将PropertyTypeEnum枚举声明为public,模板才能直接访问枚举值
  • *ngFor循环时用as语法把当前FormGroup的value存为临时变量,简化模板取值
  • 响应式表单不要和[(ngModel)]混用,直接用formControlName绑定value字段即可

对应代码修改示例:

// 组件类顶部暴露枚举,模板才能访问
public PropertyTypeEnum = PropertyTypeEnum;
// 存储初始表单值用于后续变更检测
private initialFormValue: any;

ngOnInit(): void {
  this.settings.forEach(item => {
    // 动态生成对应类型的校验器
    const validators = [];
    if (!item.isNullable) validators.push(Validators.required);
    switch(item.type) {
      case PropertyTypeEnum.String:
        validators.push(Validators.minLength(item.minValue ?? 2));
        break;
      case PropertyTypeEnum.Int:
        validators.push(Validators.pattern(/^-?\d+$/));
        item.minValue !== undefined && validators.push(Validators.min(item.minValue));
        item.maxValue !== undefined && validators.push(Validators.max(item.maxValue));
        break;
    }
    this.formArray.push(this.formBuilder.group({
      key: [item.key],
      type: [item.type],
      name: [item.displayName],
      value: [item.value, validators],
      originalValue: [item.originalValue]
    }))
  });
  // 存储初始值用于变更检测
  this.initialFormValue = JSON.parse(JSON.stringify(this.form.value));
  // 只需监听整个表单变化即可,无需重复监听formArray
  this.form.valueChanges.pipe(debounceTime(500)).subscribe(() => {
    // 深比较当前值和初始值判断是否有变更,替换你原来错误的判断逻辑
    this.areChangesDetected = JSON.stringify(this.form.value) !== JSON.stringify(this.initialFormValue);
  });
}

// 单个项重置方法
reset(index: number): void {
  const originalValue = this.formArray.at(index).get('originalValue')?.value;
  this.formArray.at(index).get('value')?.setValue(originalValue);
}

模板修改示例:

<form [formGroup]="form" class="group-wrapper">
  <div class="d-flex item-row" *ngFor="let group of formArray.controls; let i = index">
    <!-- 把当前FormGroup的value存为setting变量,直接读取元信息 -->
    <ng-container *ngIf="group.value as setting">
      <div class="item flex-fill d-flex" [ngSwitch]="setting.type" [formGroupName]="i">
        <span>{{setting.name}}</span>

        <!-- 布尔类型下拉框 -->
        <select *ngSwitchCase="PropertyTypeEnum.Boolean" class="flex-grow-1" formControlName="value">
          <option [value]="'0'">False</option>
          <option [value]="'1'">True</option>
        </select>

        <!-- 数字类型输入框 -->
        <input *ngSwitchCase="PropertyTypeEnum.Int" type="number" class="flex-grow-1" formControlName="value">
      </div>
    </ng-container>

    <button class="remove-relation" (click)="reset(i)">
      <fa-icon [icon]="faUndo"></fa-icon>
    </button>
  </div>
</form>

问题2:contenteditable非标准控件的表单绑定

有两种实现方案,可根据复用需求选择:

方案1:简单事件绑定(无需额外定义指令)

直接监听div的输入、失焦事件,手动同步值到表单控件:

<div 
  contenteditable 
  *ngSwitchCase="PropertyTypeEnum.String"
  class="flex-grow-1" 
  [id]="setting.key" 
  [innerText]="group.get('value')?.value"
  (input)="onContentEditableChange($event, i)"
></div>

组件类新增处理方法:

onContentEditableChange(event: InputEvent, index: number): void {
  const inputValue = (event.target as HTMLElement).innerText.trim();
  this.formArray.at(index).get('value')?.setValue(inputValue);
}

方案2:自定义ControlValueAccessor指令(可复用,用法和原生输入框完全一致)

定义contenteditable适配指令:

@Directive({
  selector: '[contenteditable][formControlName],[contenteditable][formControl]',
  standalone: true // 非独立模块移除该字段,添加到对应NgModule的declarations数组
})
export class ContenteditableValueAccessor implements ControlValueAccessor {
  constructor(private elementRef: ElementRef<HTMLElement>) {}

  onChange = (value: any) => {};
  onTouched = () => {};

  @HostListener('input', ['$event'])
  onInput(event: InputEvent): void {
    this.onChange((event.target as HTMLElement).innerText.trim());
  }

  @HostListener('blur')
  onBlur(): void {
    this.onTouched();
  }

  writeValue(value: any): void {
    this.elementRef.nativeElement.innerText = value ?? '';
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
}

使用时直接给contenteditable的div加formControlName="value"即可,无需手动绑定事件:

<div 
  contenteditable 
  *ngSwitchCase="PropertyTypeEnum.String"
  class="flex-grow-1" 
  formControlName="value"
  [id]="setting.key"
></div>

内容的提问来源于stack exchange,提问作者Nicke Manarin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 21:24:03