Question

How Angular Material mat-form-field Works with MatFormFieldControl

typescriptangularangular-material

Question

In Angular Material, I am trying to create reusable wrapper components around form controls.

I created a custom field component that wraps a Material mat-form-field and uses content projection:

<mat-form-field>
  <ng-content></ng-content>
  <mat-hint align="start"><strong>{{ hint }}</strong></mat-hint>
  <mat-hint align="end">{{ message.value.length }} / 256</mat-hint>
  <mat-error>This field is required</mat-error>
</mat-form-field>

Then I created a textbox component that projects an Angular Material input into that wrapper:

<field hint="hint">
  <input
    matInput
    [placeholder]="placeholder"
    [value]="value"
    (change)="onChange($event)"
    (keydown)="onKeydown($event)"
    (keyup)="onKeyup($event)"
    (keypress)="onKeypress($event)"
  />
</field>

And I use it like this:

<textbox value="test" hint="my hint"></textbox>

At runtime, Angular Material logs this error:

mat-form-field must contain a MatFormFieldControl

It seems like the mat-form-field does contain the input[matInput], but only through ng-content projection inside another component.

Why does this error happen, and what is the correct way to build reusable Angular Material form-field wrapper components?

Short Answer

By the end of this page, you will understand why mat-form-field requires a direct Angular Material form control, how Angular content projection affects component queries, and how to build reusable wrapper components correctly in Angular Material.

Concept

Angular Material's mat-form-field is not just a visual container. It actively looks for a child control that implements the MatFormFieldControl contract.

Examples of controls that work with mat-form-field include:

  • input with matInput
  • textarea with matInput
  • mat-select
  • custom components that explicitly implement MatFormFieldControl

The key detail is that mat-form-field does not simply search the final rendered DOM in a generic way. It uses Angular's component and directive querying rules to find a compatible control in the expected view/content relationship.

When you wrap things like this:

<textbox>
  <field>
    <mat-form-field>
      <ng-content></ng-content>
    </mat-form-field>
  </field>
</textbox>

and the actual input matInput belongs to another component's template, Angular Material may not see it as the projected control that mat-form-field expects.

That is why the error appears:

mat-form-field must contain a MatFormFieldControl

This matters because many Angular libraries, especially UI libraries, rely on Angular's component boundaries, dependency injection, and directive queries. Even if the final browser DOM looks correct, Angular's internal component structure is what determines whether components can find each other.

In practice, reusable wrapper components around Angular Material controls usually work best in one of these ways:

  • put the mat-form-field and the actual control in the same component template
  • pass configuration into a shared component instead of projecting the control from another wrapper
  • create a true custom form-field control by implementing MatFormFieldControl

So the real concept is not just mat-form-field, but how Angular component projection and Angular Material control detection work together.

Mental Model

Think of mat-form-field like a power strip that only works if it can recognize the plug type.

  • mat-form-field = the power strip
  • matInput / mat-select / custom MatFormFieldControl = approved plug types
  • content projection = passing a plug through a wall opening

Even if you can visually see the plug near the power strip, the strip only works if Angular Material can detect that the plug is the right kind and is connected in the expected place.

So this is not about what the HTML looks like in DevTools. It is about what Angular sees in terms of component ownership and projected content.

Take Quiz

Syntax and Examples

Core idea

A working Angular Material form field usually looks like this:

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

Here, mat-form-field can directly find the input with the matInput directive.

A reusable component that works

Instead of projecting the input through another wrapper, place the mat-form-field and the control in the same template:

<mat-form-field>
  <input
    matInput
    [placeholder]="placeholder"
    [value]="value"
    (input)="onInput($event)"
  />
  <mat-hint align="start">{{ hint }}</mat-hint>
  <mat-hint align="end">{{ value?.length || 0 }} / 256</mat-hint>
  <mat-error *ngIf="required">This field is required</mat-error>
</mat-form-field>

Example component:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-textbox',
  templateUrl: './textbox.component.html'
})
export class TextboxComponent {
  @Input() hint = '';
  @Input() placeholder = '';
  @Input() value = '';
  @Input() required = false;

  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.value = target.value;
  }
}

Usage:

<app-textbox
  [value]="'test'"
  hint="My hint"
  placeholder="Enter text"
></app-textbox>

When projection is okay

Projection can still be useful for plain layout components, but for Angular Material form-field controls you must ensure the projected content is something mat-form-field can detect properly.

A simpler projected version may work when the projected control is a direct child from the parent usage site:

<app-field>
  <input matInput placeholder="Name" />
</app-field>

with:

<mat-form-field>
  <ng-content></ng-content>
  <mat-hint>{{ hint }}</mat-hint>
</mat-form-field>

But when you add another wrapper component like textbox that hides the actual input inside its own template, the relationship changes and Angular Material can no longer detect the control the same way.

Step by Step Execution

Consider this simplified version:

<!-- field.component.html -->
<mat-form-field>
  <ng-content></ng-content>
</mat-form-field>
<!-- textbox.component.html -->
<field>
  <input matInput />
</field>

What happens step by step

  1. Angular creates the textbox component.
  2. Inside textbox, Angular sees a field component.
  3. The input matInput belongs to the textbox template, not the field template.
  4. The field component renders its own template, which contains mat-form-field and <ng-content>.
  5. Angular projects the input matInput into the field component's content slot.
  6. mat-form-field tries to find a child control implementing MatFormFieldControl.
  7. Because of the component/query boundary, Angular Material does not detect the projected control in the way it requires.
  8. Angular Material throws:
mat-form-field must contain a MatFormFieldControl

Working version

Now compare that with this:

<!-- textbox.component.html -->
<mat-form-field>
  <input matInput />
</mat-form-field>

Step by step:

  1. Angular creates textbox.
  2. textbox template contains both mat-form-field and input matInput.
  3. mat-form-field queries for its control.
  4. It finds the matInput control immediately.
  5. No error is thrown.

The lesson: Angular component structure matters more than the final rendered HTML.

Real World Use Cases

Reusable input components in design systems

Companies often build shared UI components such as:

  • text boxes
  • email fields
  • password fields
  • search boxes
  • number inputs

These components usually need labels, hints, validation messages, and consistent styling. Angular Material supports this well, but the control must be structured so mat-form-field can detect it.

Form validation

A common use case is showing validation messages:

<mat-form-field>
  <input matInput [formControl]="emailControl" placeholder="Email" />
  <mat-error *ngIf="emailControl.hasError('required')">
    Email is required
  </mat-error>
</mat-form-field>

Shared form components across an app

Teams often create components such as:

  • app-textbox
  • app-password-field
  • app-date-field
  • app-select-field

These should usually own their internal mat-form-field and Material control instead of splitting them across multiple wrapper layers.

Custom domain-specific controls

If you need a complex control such as:

  • phone number input with country code
  • address input
  • currency input
  • date range input

then building a custom component that implements MatFormFieldControl is often the right long-term solution.

Take Quiz

Real Codebase Usage

In real Angular codebases, developers typically use one of these patterns.

1. Component owns the Material control

This is the most common approach.

<mat-form-field>
  <input matInput [formControl]="control" />
  <mat-error *ngIf="control.invalid">Invalid value</mat-error>
</mat-form-field>

Why it is common:

  • simple to understand
  • works reliably with Angular Material
  • easy to test
  • easy to connect to reactive forms

2. Use ControlValueAccessor for reusable inputs

If you want a reusable custom input component that works with Angular forms, implement ControlValueAccessor.

This lets your component behave like a normal form control.

Typical pattern:

  • custom component owns its internal input
  • component exposes value through ControlValueAccessor
  • parent form uses formControlName or ngModel

3. Implement MatFormFieldControl for deep Material integration

If you want your custom component itself to behave like a Material form-field control, implement MatFormFieldControl.

This is useful when the component is not just wrapping one input, but is truly a custom control.

4. Guard clauses and validation patterns

Real projects often combine Material fields with validation logic:

if (this.form.invalid) {
  this.form.markAllAsTouched();
  return;
}

This works well when each reusable field integrates cleanly with Angular forms instead of only forwarding DOM events.

5. Prefer reactive forms in larger apps

Larger Angular apps often prefer FormControl and FormGroup because they make validation, error display, and shared field components easier to manage.

Common Mistakes

1. Assuming rendered DOM is all that matters

A common mistake is thinking:

If the input appears inside mat-form-field in the browser, Angular Material should accept it.

That is not always true. Angular cares about component templates, directives, and queries, not only the final DOM structure.

2. Wrapping mat-form-field and the control in separate components

Broken pattern:

<!-- field.component.html -->
<mat-form-field>
  <ng-content></ng-content>
</mat-form-field>
<!-- textbox.component.html -->
<field>
  <input matInput />
</field>

This can cause the MatFormFieldControl detection to fail.

3. Using [value] without proper form integration

This is often too limited:

<input matInput [value]="value" />

Better options in Angular apps:

  • reactive forms with [formControl]
  • template-driven forms with [(ngModel)]
  • custom components with ControlValueAccessor

4. Using DOM events instead of Angular form patterns

This works, but is often not ideal for reusable fields:

<input
  matInput
  (change)="onChange($event)"
  (keyup)="onKeyup($event)"
/>

For real forms, Angular form APIs are usually better because they handle:

  • validation state
  • touched/dirty state
  • disabled state
  • form submission

5. Forgetting that custom controls need explicit integration

If your component is more than a simple wrapper, Angular Material will not automatically treat it like a valid form-field control. You may need to implement MatFormFieldControl.

6. Mixing old and new Angular Material APIs

Depending on Angular Material version, examples online may differ. Always check that the example matches your version, especially for hints, appearance, and form APIs.

Comparisons

ApproachWorks with mat-form-field easily?Best forComplexity
Plain mat-form-field + input matInput in same templateYesSimple inputsLow
Wrapper component that owns both field and inputYesReusable design-system inputsLow to medium
Wrapper component with projected input from another wrapperOften problematicAdvanced compositionMedium
Custom component implementing ControlValueAccessorYes, for Angular formsReusable form componentsMedium
Custom component implementing MatFormFieldControlYes, with deep Material integrationComplex custom controlsHigh

ControlValueAccessor vs MatFormFieldControl

ConceptPurpose
ControlValueAccessorMakes a custom component behave like an Angular form control
MatFormFieldControlMakes a custom component behave like a control inside mat-form-field

You may need:

  • only ControlValueAccessor for form integration
  • both ControlValueAccessor and MatFormFieldControl for full Angular Material form-field integration
Take Quiz

Cheat Sheet

Quick rules

  • mat-form-field must contain a control Angular Material recognizes.
  • Valid controls include input[matInput], textarea[matInput], mat-select, and custom MatFormFieldControl implementations.
  • Do not assume final DOM structure is enough.
  • Prefer putting mat-form-field and its control in the same component template.
  • For reusable custom fields, use ControlValueAccessor.
  • For custom Material-compatible controls, implement MatFormFieldControl.

Safe pattern

<mat-form-field>
  <input matInput [formControl]="control" />
  <mat-error *ngIf="control.invalid">Invalid</mat-error>
</mat-form-field>

Risky pattern

<!-- Wrapper A -->
<mat-form-field>
  <ng-content></ng-content>
</mat-form-field>
<!-- Wrapper B -->
<wrapper-a>
  <input matInput />
</wrapper-a>

Best fix for your case

Move the input matInput into the same component template as the mat-form-field, or build a real custom control.

If you need reusable inputs

  • simple wrapper: component owns both field and input
  • form integration: implement ControlValueAccessor
  • Material integration: implement MatFormFieldControl

FAQ

Why does Angular Material say mat-form-field must contain a MatFormFieldControl?

Because mat-form-field could not find a compatible child control such as input[matInput], textarea[matInput], mat-select, or a custom MatFormFieldControl.

Why does it fail even though the input appears inside the rendered HTML?

Because Angular Material uses Angular component/directive queries, not just the final browser DOM structure.

Can I wrap mat-form-field in a reusable component?

Yes, but the safest approach is for that reusable component to also own the actual Material control in the same template.

Should I use ng-content with Angular Material form fields?

You can in some cases, but extra wrapper layers often cause detection problems. Test carefully.

What if I want my own custom input component to work inside mat-form-field?

Implement MatFormFieldControl. If it also needs to work with Angular forms, implement ControlValueAccessor too.

Is ControlValueAccessor enough by itself?

Not always. It makes your component work with Angular forms, but not automatically with Angular Material's mat-form-field.

What is the simplest fix for this error?

Place mat-form-field and the input matInput in the same component template.

Take Quiz

Mini Project

Description

Build a reusable Angular Material text input component for a company design system. The component should display a hint, a character count, and validation feedback without splitting the mat-form-field and the matInput across different wrapper components.

Goal

Create a reusable app-textbox component that works cleanly with Angular Material and Angular forms.

Requirements

[ "Create a textbox component that owns both mat-form-field and input matInput in the same template.", "Accept placeholder, hint, and maxLength as inputs.", "Display a live character count.", "Use a FormControl to manage the value.", "Show an error message when the field is required and empty." ]

Take Quiz

Keep learning

Related questions

View more