在Angular Material中使用ControlValueAccessor进行错误验证

15

我正在尝试在自定义 Material 输入框中使用 ControlValueAccessor 应用 Error Validation 样式。自从应用了这个自定义组件后,所有带有 formControlName/FormBuilders 的必需、最小长度等的红色边框验证状态都不显示了。在 Angular Material 文本框中,它可以原生地(开箱即用)工作,直到应用了自定义控件。

目标是让自定义文本框与表单验证一起工作。这在 matInput 文本框中自然显示。

更新: 发布了答案;但不确定是否最有效,正在尝试让 Saloo 答案也能工作(如果有人能发布 stackbliz,那就太好了),欢迎任何更有效的选项。

显示输入框截图

TypeScript:

import { Component, OnInit, Input, ViewChild, EventEmitter, Output, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-input-textbox',
  templateUrl: './input-textbox.component.html',
  styleUrls: ['./input-textbox.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextboxComponent),
      multi: true
    }
  ]
})

export class InputTextboxComponent implements OnInit, ControlValueAccessor {
  @Input() MaxLength: string;
  @Input() Value: string;
  @Input() type: string;
  @Input() Label: string;
  @Input() PlaceHolder: string;
  @Output() saveValue = new EventEmitter();
  @Output() onStateChange = new EventEmitter();

  disabled: boolean;

  constructor() { }

  ngOnInit() {
  }

  saveValueAction(e) {
    this.saveValue.emit(e.target.value);
  }

  onChange(e) {
    this.Value = e;
  }

  onTouched() {
    this.onStateChange.emit();
  }

  writeValue(value: any) {
    this.Value = value ? value : '';
  }

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

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

  setDisabledState(isDisabled) { this.disabled = isDisabled; }
}

HTML:

<div class="input-wrap">
    <mat-form-field appearance="outline">
        <mat-label>{{Label}}</mat-label>   
        <input matInput 
            [attr.maxlength] = "MaxLength"
            [value]="Value ? Value : ''"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
            (input)="onChange($event.target.value)"
        >
    </mat-form-field>
</div>

尝试将此答案与Angular Material文本框的自然错误样式相结合, 在Angular中使用ControlValueAccessor继承验证


http://prideparrot.com/blog/archive/2019/2/applying_validation_custom_form_component - VJAI
你能否设置一个可工作的 StackBlitz? - Ron
为什么这个问题被投票否决了?我提出了一个扎实的问题,并在下面给出了我的深思熟虑的研究答案,寻求更优化的代码。 - user12425844
3个回答

8

我也遇到了同样的问题。我尝试了所有方法,最终使用了以下方法解决:

在自定义组件上添加此监听器。您也可以在“失去焦点”事件上执行此操作。

@HostListener('focusout', ['$event.target'])
  onFocusout() {
    this.onTouched();
  }

并且在设置任何值时调用onTouched。
 writeValue(value: any) {
    this.onTouched();
    this.Value = value ? value : '';
}

1
如果有人能让 StackBlitz 在这个项目中正常工作,欢迎将代码粘贴到这个答案中。看起来非常高效,如果可以正常运行的话。 - user12425844
在 StackBlitz 或某个可工作的演示之前,这不能用作答案。 - user12425844

6

我借鉴了您的答案和提供的链接,得出了以下解决方案:


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
  }]
})
export class CustomInputComponent implements OnInit, ControlValueAccessor {
  ...
  _control: NgControl;
  disabled: boolean;

  constructor(@Inject(INJECTOR) private injector: Injector) {
  }

  ngOnInit() {
    this._control = this.injector.get(NgControl);
  }
  
  ...

custom-input.component.html

<div class="input-wrap">
    <mat-form-field appearance="outline">
        <mat-label>{{Label}}</mat-label>   
        <input matInput
            [formControl]="_control.control" // <== this what makes it work
            [attr.maxlength]="MaxLength"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
        >
    </mat-form-field>
</div>

注意:我没有绑定到MatInput的输出(是的)。将控件传递给MatInput的formControl指令会为我们处理。

这里为您制作了一个示例


如果在 Angular 12 中启用了 strictTemplate,则无法正常工作。因为 _control.control 返回一个 AbstractControl。虽然您可以对控件进行类型转换,但需要将该方法移动到 ngAfterViewInit 中。 - LeO

3

这将通过Angular Material创建错误验证。

TypeScript:

import { Component, OnInit, Input, EventEmitter, Output, forwardRef, Injector } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, NgForm, FormGroupDirective, NgControl } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';

export class CustomFieldErrorMatcher implements ErrorStateMatcher {
  constructor(private customControl: FormControl,private errors:any) { }

  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return this.customControl && this.customControl.touched &&(this.customControl.invalid || this.errors);
  }
}

@Component({
  selector: 'app-input-textbox',
  templateUrl: './input-textbox.component.html',
  styleUrls: ['./input-textbox.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextboxComponent),
      multi: true
    }
  ]
})

export class InputTextboxComponent implements OnInit, ControlValueAccessor {
  @Input() MaxLength: string;
  @Input() FocusIn: boolean;
  @Input() Width: string;
  @Input() Value: string;
  @Input() type: string;
  @Input() Label: string;
  @Input() Hint: string;
  @Input() PlaceHolder: string;
  @Output() saveValue = new EventEmitter();
  @Output() onStateChange = new EventEmitter();
  @Input() errors: any = null;
  disabled: boolean;
  control: FormControl;

  constructor(public injector: Injector) {}

  ngOnInit(){}

  ngAfterViewInit(): void {
    const ngControl: NgControl = this.injector.get(NgControl, null);
    if (ngControl) {
      setTimeout(() => {
        this.control = ngControl.control as FormControl;
      })
    }
  }

  saveValueAction(e) {
    this.saveValue.emit(e.target.value);
  }

  //control value accessor init
  writeValue(value: any) {
    this.Value = value ? value : '';
  }

  onChange(e) {
    this.Value = e;
  }

  onTouched() {
    this.onStateChange.emit();
  }

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

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

  setDisabledState(isDisabled) { this.disabled = isDisabled; }

  errorMatcher() {
    return new CustomFieldErrorMatcher(this.control,this.errors)
  }

  readonly errorStateMatcher: ErrorStateMatcher = {
    isErrorState: (ctrl: FormControl) => (ctrl && ctrl.invalid)
  };

}

HTML

<div class="input-wrap">
    <mat-form-field>
        <mat-label>{{Label}}</mat-label>   
        <input 
            matInput 
            [attr.maxlength] = "MaxLength"
            [value]="Value ? Value : ''"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
            [ngModel]="Value" 
            [errorStateMatcher]="errorMatcher()"

            (input)="onChange($event.target.value)"
            (blur)="onTouched()"
            (change)="saveValueAction($event)"
            (ngModelChange)="Value=$event;onChange($event)"
        >
        <mat-hint>{{Hint}}</mat-hint>
    </mat-form-field>
</div>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接