MatFormFieldControl: разбираем ошибку раз и навсегда

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему ваша Angular форма кричит об ошибке?

Вы когда-нибудь сталкивались с ситуацией: пишете красивую форму на Angular Material, всё кажется идеальным, но консоль браузера внезапно выдает сообщение:

mat-form-field must contain a MatFormFieldControl

Или, если вы используете строгий режим TypeScript, может появиться что-то вроде:

Error: mat-form-field must contain a MatFormFieldControl. Did you forget to add a directive that implements MatFormFieldControl?

Это одна из самых популярных ошибок на Stack Overflow (тысячи просмотров!), и сегодня мы разберем её вдоль и поперек. Вы узнаете не только как исправить эту проблему, но и почему она возникает. А главное — научитесь создавать свои собственные контролы для Angular Material, если стандартный MatInput вам не подходит.

Поехали! 🚀

Что такое MatFormFieldControl и зачем он нужен?

Прежде чем лечить ошибку, давайте поймем анатомию Angular Material. mat-form-field — это контейнер, который добавляет Material Design-стилизацию (лайбл, подчеркивание, подсказки, ошибки). Но сам по себе он ничего не отображает. Ему нужен "ребенок" — директива или компонент, который реализует интерфейс MatFormFieldControl.

Этот интерфейс (или абстрактный класс) описывает, как контрол взаимодействует с формой: как получить значение, как показать ошибку, как обработать фокус. Стандартная реализация — это MatInput. Если вы пишете:

<mat-form-field>
  <input matInput placeholder="Имя">
</mat-form-field>

— то директива matInput автоматически реализует MatFormFieldControl.

Ошибка возникает, когда Angular не находит ни одного дочернего элемента с такой реализацией. Например:

<mat-form-field>
  <input placeholder="Имя"> <!-- Нет matInput! -->
</mat-form-field>

Или когда вы пытаетесь использовать кастомный компонент, который не подключил интерфейс.

Типичные причины ошибки (и как их исправить)

1. Забыли добавить директиву matInput

Самая банальная причина. Вы просто не указали matInput на элементе <input>.

Неправильно:

<mat-form-field>
  <input placeholder="Email">
</mat-form-field>

Правильно:

<mat-form-field>
  <input matInput placeholder="Email">
</mat-form-field>

Проверьте также, что вы импортировали MatInputModule в ваш модуль:

import { MatInputModule } from '@angular/material/input';

@NgModule({
  imports: [MatInputModule]
})
export class AppModule { }

2. Используете не тот HTML-элемент

MatInput работает только с <input> и <textarea>. Если вы попытаетесь обернуть в mat-form-field обычный <div> или <select> (без mat-select), получите ошибку.

Неправильно:

<mat-form-field>
  <div>Какой-то контент</div>
</mat-form-field>

Правильно: используйте соответствующие Material-компоненты:

<mat-form-field>
  <mat-select placeholder="Город">
    <mat-option value="1">Москва</mat-option>
  </mat-select>
</mat-form-field>

Для mat-select нужно импортировать MatSelectModule.

3. Кастомный компонент не реализует MatFormFieldControl

Это более сложный случай. Допустим, вы создали свой компонент для ввода даты или рейтинга. Angular Material не знает о нём. Нужно явно "научить" его.

Давайте создадим простой кастомный контрол для ввода чисел с шагом.

Создаем кастомный MatFormFieldControl на TypeScript

Предположим, мы хотим сделать компонент app-stepper-input, который выглядит как поле ввода, но имеет кнопки "+" и "-".

Шаг 1: Создаем компонент

import { Component, forwardRef, Input, ViewChild, ElementRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material/form-field';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-stepper-input',
  templateUrl: './stepper-input.component.html',
  styleUrls: ['./stepper-input.component.scss'],
  providers: [
    {
      provide: MatFormFieldControl,
      useExisting: forwardRef(() => StepperInputComponent)
    }
  ]
})
export class StepperInputComponent implements MatFormFieldControl<number>, ControlValueAccessor {
  static nextId = 0;
  @ViewChild('input') input: ElementRef;

  stateChanges = new Subject<void>();
  id = `stepper-input-${StepperInputComponent.nextId++}`;
  placeholder: string = '';
  focused = false;
  touched = false;
  controlType = 'stepper-input';
  private _value: number = 0;
  private _disabled = false;

  get empty() {
    return !this._value;
  }

  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @Input()
  get value(): number | null {
    return this._value;
  }
  set value(val: number | null) {
    this._value = val || 0;
    this.stateChanges.next();
  }

  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(val: boolean) {
    this._disabled = val;
    this.stateChanges.next();
  }

  get errorState(): boolean {
    // Можно добавить свою логику валидации
    return false;
  }

  setDescribedByIds(ids: string[]) {}
  onContainerClick(event: MouseEvent) {
    this.input?.nativeElement.focus();
  }

  // ControlValueAccessor
  onChange: any = () => {};
  onTouched: any = () => {};

  writeValue(value: number): void {
    this.value = value;
  }

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

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

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

  increment() {
    if (!this.disabled) {
      this.value++;
      this.onChange(this.value);
    }
  }

  decrement() {
    if (!this.disabled) {
      this.value--;
      this.onChange(this.value);
    }
  }
}

Шаг 2: Шаблон компонента

<div class="stepper-container">
  <button (click)="decrement()" [disabled]="disabled">-</button>
  <input #input
         [value]="value"
         [                

Блоги

Рекомендаций по книгам