Как получить пользовательский MatFormFieldControl для отображения недопустимого состояния

Я следил за документацией по угловым материалам, в которой рассматривается создание пользовательского элемента управления полем формы: https://material.angular.io/guide/creating-a-custom-form-field-control

Он удобно пропускает шаблон и полный пример реактивных форм, так что я копался в попытке соединить все это.

У меня был удар, с переменным успехом. Хотя есть и другие проблемы, я хотел бы сначала понять, как я могу заставить это настраиваемое поле распознавать, когда оно invalid так что я могу выполнить <mat-error> Вы видите ниже (я удалил *ngIf просто чтобы я мог видеть состояние invalid). {{symbolInput.invalid}} всегда falseкогда на самом деле это должно быть true как поле обязательно для заполнения!

Использование пользовательского шаблона MatFormFieldControl:

<mat-form-field class="symbol">
    <symbol-input
      name="symbol"
      placeholder="Symbol"
      ngModel
      #symbolInput="ngModel"
      [(ngModel)]="symbol"
      required></symbol-input>
    <button
      mat-button matSuffix mat-icon-button
      *ngIf="symbol && (symbol.asset1 || symbol.asset2)"
      aria-label="Clear"
      (click)="clearSymbol()">
      <mat-icon>close</mat-icon>
    </button>
    <mat-error >{{symbolInput.invalid}}</mat-error>
  </mat-form-field>

Пользовательский класс MatFormFieldControl:

export interface AssetSymbol {
  asset1: string, asset2: string
}

@Component({
  selector: 'symbol-input',
  templateUrl: './symbol-input.component.html',
  styleUrls: ['./symbol-input.component.css'],
  providers: [{ provide: MatFormFieldControl, useExisting: SymbolInputComponent}]
})
export class SymbolInputComponent implements MatFormFieldControl<AssetSymbol>, OnDestroy {
  static nextId = 0;

  stateChanges = new Subject<void>();
  parts: FormGroup;
  focused = false;
  errorState = false;
  controlType = 'symbol-input';
  onChangeCallback;

  @HostBinding() id = `symbol-input-${SymbolInputComponent.nextId++}`;
  @HostBinding('class.floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }
  @HostBinding('attr.aria-describedby')
  describedBy = '';
  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  get empty() {
    let n = this.parts.value;
    return !n.asset1 && !n.asset2;
  }

  @Input()
  get value(): AssetSymbol | null {
    let n = this.parts.value;
    return { asset1: n.asset1, asset2: n.asset2};
  }
  set value(symbol: AssetSymbol | null) {
    symbol = symbol || { asset1: "", asset2: ""};
    this.parts.setValue({asset1: symbol.asset1, asset2: symbol.asset2});
    this.stateChanges.next();
  }
  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  private _placeholder: string;
  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  private _required = false;
  @Input()
  get disabled() {
    return this._disabled;
  }
  set disabled(dis) {
    this._disabled = coerceBooleanProperty(dis);
    this.stateChanges.next();
  }
  private _disabled = false;

  constructor(
    fb: FormBuilder,
    @Optional() @Self() public ngControl: NgControl,
    private fm: FocusMonitor,
    private elRef: ElementRef<HTMLElement>) {

    this.parts = fb.group({'asset1': '', 'asset2': ''});
    // Setting the value accessor directly (instead of using
    // the providers) to avoid running into a circular import.
    if (this.ngControl != null) this.ngControl.valueAccessor = this;

    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });

    this.stateChanges.subscribe(() => {
      this.expandInput(this.value.asset1.length);
      if (this.onChangeCallback) {
        this.onChangeCallback(this.value);
        if (this.required) {
          const symbol = this.value;
          if (!symbol.asset1 || !symbol.asset2) {
            this.errorState = true;
          } else {
            this.errorState = false;
          }
        }
      }
    });
  }

  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() != 'input') {
      this.elRef.nativeElement.querySelector('input').focus();
    }
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }

  onKeyup() {
    this.stateChanges.next();
  }

  static ASSET1_INPUT_SIZE = 2;
  asset1InputSize = SymbolInputComponent.ASSET1_INPUT_SIZE;
  expandInput(currentSize) {
    //const currentSize = (event.target as HTMLInputElement).value.length;
    if (currentSize >= 3) {
      this.asset1InputSize = currentSize;
    } else {
      this.asset1InputSize = SymbolInputComponent.ASSET1_INPUT_SIZE;
    }
  }

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

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

  registerOnTouched(fn: any) {
  }

}

Символ-input.component.html:

<div [formGroup]="parts" >
  <input class="asset asset1" formControlName="asset1" (keyup)="onKeyup()" [size]="asset1InputSize" maxlength="5">
  <span class="input-spacer">&frasl;</span>
  <input class="asset asset2" formControlName="asset2" size="6" maxlength="5">
</div>

Будет ли кто-то достаточно любезен, чтобы указать мне правильное направление?

** ОБНОВЛЕНО ** symbolInput.invalid флаг теперь устанавливается после подписки на this.ngControl.valueChanges и настройка this.ngControl.control.setErrors:

constructor(
    fb: FormBuilder,
    @Optional() @Self() public ngControl: NgControl,
    private fm: FocusMonitor,
    private elRef: ElementRef<HTMLElement>) {

    this.parts = fb.group({'asset1': ['',[Validators.required]], 'asset2': ['',[Validators.required]]});

    if (this.ngControl != null) this.ngControl.valueAccessor = this;

    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
    this.ngControl.valueChanges.subscribe(()=>{
      this.expandInput(this.value.asset1.length);
      if (this.required) {
        if (this.parts.invalid) {
          this.errorState = true;
          this.ngControl.control.setErrors({ "invalidSymbol": true });
        } else {
          this.errorState = false;
          this.ngControl.control.setErrors(null);
        }
      }
    });
    this.stateChanges.subscribe(() => {
      if (this.onChangeCallback) {
        this.onChangeCallback(this.value);
      }
    });
  }

Пожалуйста, сообщите, если вы думаете, что это может быть улучшено.

2 ответа

Ваша реализация выглядит хорошо, и вы всегда получаете invalid как false так как вы не добавили никакой проверки.

Вы можете добавить проверку для asset1 а также asset2 поэтому измените следующую строку

 this.parts = fb.group({'asset1': '', 'asset2': ''});

в

 this.parts = this.fb.group({
      asset1: ['', Validators.required],
      asset2: ['', Validators.required,  Validators.minLength(6)]
 });

Я сделал вот что:

      get errorState(): boolean {
return (this.model.invalid && this.model.dirty) || (this.ngControl?.invalid && this.ngControl?.dirty);

}

Где model мой местный FormControl и ngControlэто родительский элемент управления. Таким образом, он возвращает состояние ошибки, когда мой элемент управления имеет ошибку или родительский элемент недействителен.

Другие вопросы по тегам