为什么此自定义 Angular 控件中的验证错误在父组件的表单中看不到?

问题描述 投票:0回答:1

我正在 Angular (16) 中开发我的第一个自定义控件,用于反应式表单,其中包含滑块和输入。如果滑块为真,则输入应该有一个值。如果不是,则输入不应该有值。

自定义组件 HTML:

<div class="benefit-field">
    <div class="slide-toggle">
        <mat-slide-toggle [checked]="slideEnabled" (change)="updateEnabled($event)">Enabled?</mat-slide-toggle>
    </div>
    <div [ngClass]="{'disabled': !slideEnabled}">
        <mat-form-field>
            <mat-label>{{ typeName }}</mat-label>
            <input type="text" matInput [value]="valueId" [disabled]="!slideEnabled" (change)="onValueChanged($event.target)" />
        </mat-form-field>
    </div>
</div>

自定义组件 TypeScript:

import { Component, forwardRef, Input } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';

@Component({
  selector: 'app-optional-benefit-field',
  templateUrl: './optional-benefit-field.component.html',
  styleUrls: ['./optional-benefit-field.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: OptionalBenefitFieldComponent
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: forwardRef(() => OptionalBenefitFieldComponent)
    },
  ]
})
export class OptionalBenefitFieldComponent implements ControlValueAccessor, Validator {
  @Input() typeId: number;
  @Input() typeName: number;

  valueId: number;
  slideEnabled = false;

  onChange = (_valueId) => { /* nothing needs to be done here, let it trickle up */ };
  onTouched = () => { /* nothing needs to be done here, let it trickle up */ };
  touched = false;
  disabled = false;

  writeValue(valueId: number): void {
    if (valueId && valueId > 0) {
      this.slideEnabled = true;
    }
    this.valueId = valueId;
  }

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

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

  markAsTouched(): void {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  updateEnabled(event: MatSlideToggleChange): void {
    this.markAsTouched();
    this.slideEnabled = event.checked;
    if (!this.slideEnabled) {
      this.onChange(null);
    } else {
      this.onChange(this.valueId);
    }
  }

  onValueChanged(input: HTMLInputElement): void {
    if (input.value && (+input.value) > 0) {
      this.onChange(+input.value);
    } else {
      this.onChange(null);
    }
  }

  validate(control: AbstractControl): ValidationErrors | null {
    return (!this.slideEnabled || (this.slideEnabled && control.value))
      ? null
      : { required: true };
  }
}

父组件HTML:

<app-optional-benefit-field
    [typeId]="..."
    [typeName]="..."
    formControlName="valueId"
    ></app-optional-benefit-field>

当我将自定义控件置于无效状态时,我可以看到

validate
返回
{ required: true }

但是,如果我查看父窗体,

.value
具有当前控件的值,但
.errors
为空。

我的自定义控件或父组件中缺少什么才能正确发送/接收验证错误?

angular angular-reactive-forms angular-custom-validators angular-controlvalueaccessor
1个回答
0
投票

这看起来像是角度发布中的一个小错误,证明。如果你愿意,你可以提出一个 github 问题。解决方案是将您的 Angular 版本降低到最新版本,例如 18。

Stackblitz 演示

无论如何,代码需要进行以下更改。

  1. 当你使用表单字段模块时,它必须有

    NgControl
    ,这可以由
    ngModel
    formControl
    提供,我使用的是
    formControl

  2. 使用反应式表单时不要使用

    [disabled]
    属性,因此我将其更改为编程式。

  3. 我将输入更改为数字,因为您只为数字编写了一些检查。

除此之外,你的代码运行得很好。

完整代码:

import { Component } from '@angular/core';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { bootstrapApplication } from '@angular/platform-browser';
import { forwardRef, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  AbstractControl,
  ControlValueAccessor,
  FormControl,
  FormGroup,
  FormsModule,
  NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ReactiveFormsModule,
  ValidationErrors,
  Validator,
  Validators,
} from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-optional-benefit-field',
  imports: [
    MatFormFieldModule,
    MatSlideToggleModule,
    CommonModule,
    MatInputModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  standalone: true,
  template: `
  <div class="benefit-field">
      <div class="slide-toggle">
          <mat-slide-toggle [checked]="slideEnabled" (change)="updateEnabled($event)">Enabled?</mat-slide-toggle>
      </div>
      <div [ngClass]="{'disabled': !slideEnabled}">
          <mat-form-field>
              <mat-label>{{ typeName }}</mat-label>
              <input type="number" matInput [formControl]="valueId" (input)="onValueChanged($event)"/>
          </mat-form-field>
      </div>
  </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: OptionalBenefitFieldComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: forwardRef(() => OptionalBenefitFieldComponent),
    },
  ],
})
export class OptionalBenefitFieldComponent
  implements ControlValueAccessor, Validator
{
  @Input() typeId!: number;
  @Input() typeName!: number;

  valueId: FormControl<number | null> = new FormControl<number | null>({
    value: null,
    disabled: true,
  });
  slideEnabled = false;

  onChange = (_valueId: any) => {
    /* nothing needs to be done here, let it trickle up */
  };
  onTouched = () => {
    /* nothing needs to be done here, let it trickle up */
  };
  touched = false;
  disabled = false;

  writeValue(valueId: number): void {
    if (valueId && valueId > 0) {
      this.slideEnabled = true;
    }
    this.valueId.patchValue(valueId);
  }

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

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

  markAsTouched(): void {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  updateEnabled(event: MatSlideToggleChange): void {
    this.markAsTouched();
    this.slideEnabled = event.checked;
    if (!this.slideEnabled) {
      this.onChange(null);
      this.valueId.disable();
    } else {
      this.onChange(this.valueId.value);
      this.valueId.enable();
    }
    this.valueId.updateValueAndValidity();
  }

  onValueChanged(event: any): void {
    const value = event?.target?.value;
    if (value && +value > 0) {
      this.onChange(+value);
    } else {
      this.onChange(null);
    }
  }

  validate(control: AbstractControl): ValidationErrors | null {
    return !this.slideEnabled || (this.slideEnabled && control.value)
      ? null
      : { required: true };
  }
}

/**
 * @title Basic slide-toggles
 */
@Component({
  selector: 'slide-toggle-overview-example',
  template: `
  <form [formGroup]="form">
    <app-optional-benefit-field
      [typeId]="1234"
      [typeName]="1234"
      formControlName="valueId"
      ></app-optional-benefit-field>
  </form>
  <hr/>
  {{form.errors | json}}
  <hr/>
  {{form.controls.valueId.errors | json}}
  <hr/>
  {{form.value | json}}
  `,
  standalone: true,
  imports: [OptionalBenefitFieldComponent, ReactiveFormsModule, CommonModule],
})
export class SlideToggleOverviewExample {
  form = new FormGroup({
    valueId: new FormControl(null),
  });
}

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.