Question
How Angular Material mat-form-field Works with MatFormFieldControl
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:
inputwithmatInputtextareawithmatInputmat-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-fieldand 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 stripmatInput/mat-select/ customMatFormFieldControl= 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.
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
- Angular creates the
textboxcomponent. - Inside
textbox, Angular sees afieldcomponent. - The
input matInputbelongs to thetextboxtemplate, not thefieldtemplate. - The
fieldcomponent renders its own template, which containsmat-form-fieldand<ng-content>. - Angular projects the
input matInputinto thefieldcomponent's content slot. mat-form-fieldtries to find a child control implementingMatFormFieldControl.- Because of the component/query boundary, Angular Material does not detect the projected control in the way it requires.
- 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:
- Angular creates
textbox. textboxtemplate contains bothmat-form-fieldandinput matInput.mat-form-fieldqueries for its control.- It finds the
matInputcontrol immediately. - 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-textboxapp-password-fieldapp-date-fieldapp-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.
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
formControlNameorngModel
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
inputappears insidemat-form-fieldin 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
| Approach | Works with mat-form-field easily? | Best for | Complexity |
|---|---|---|---|
Plain mat-form-field + input matInput in same template | Yes | Simple inputs | Low |
| Wrapper component that owns both field and input | Yes | Reusable design-system inputs | Low to medium |
| Wrapper component with projected input from another wrapper | Often problematic | Advanced composition | Medium |
Custom component implementing ControlValueAccessor | Yes, for Angular forms | Reusable form components | Medium |
Custom component implementing MatFormFieldControl | Yes, with deep Material integration | Complex custom controls | High |
ControlValueAccessor vs MatFormFieldControl
| Concept | Purpose |
|---|---|
ControlValueAccessor | Makes a custom component behave like an Angular form control |
MatFormFieldControl | Makes a custom component behave like a control inside mat-form-field |
You may need:
- only
ControlValueAccessorfor form integration - both
ControlValueAccessorandMatFormFieldControlfor full Angular Material form-field integration
Cheat Sheet
Quick rules
mat-form-fieldmust contain a control Angular Material recognizes.- Valid controls include
input[matInput],textarea[matInput],mat-select, and customMatFormFieldControlimplementations. - Do not assume final DOM structure is enough.
- Prefer putting
mat-form-fieldand 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.
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."
]
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.