Template-Driven Forms with Angular Material

๐Ÿ“‹ Table of Contents โ–พ
  1. Complete Material Login Form
  2. Common Mistakes

Angular Material’s form components (mat-form-field, mat-input, mat-select, mat-datepicker) integrate seamlessly with both template-driven and reactive forms. They provide consistent, accessible, and visually polished form UI with built-in error state display through <mat-error>. For the BlogApp’s login and registration forms, Angular Material provides the visual polish of a production-quality auth UI without writing custom CSS for every form element.

Complete Material Login Form

import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule }     from '@angular/material/input';
import { MatButtonModule }    from '@angular/material/button';
import { MatIconModule }      from '@angular/material/icon';
import { MatCheckboxModule }  from '@angular/material/checkbox';

@Component({
  selector:   'app-login',
  standalone:  true,
  imports: [
    FormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule,
    MatCheckboxModule,
  ],
  template: `
    <div class="login-card">
      <h1>Sign In</h1>

      <form #loginForm='ngForm' (ngSubmit)="onSubmit(loginForm)" novalidate>

        <!-- mat-form-field wraps the input and provides consistent styling โ”€โ”€ -->
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Email Address</mat-label>
          <mat-icon matPrefix>email</mat-icon>
          <input matInput
                 type="email"
                 name="email"
                 [(ngModel)]="credentials.email"
                 required
                 email
                 #emailCtrl="ngModel"
                 autocomplete="email">
          <!-- mat-error shown automatically when control is invalid+touched โ”€ -->
          <mat-error *ngIf="emailCtrl.hasError('required')">Email is required</mat-error>
          <mat-error *ngIf="emailCtrl.hasError('email')">Enter a valid email</mat-error>
        </mat-form-field>

        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Password</mat-label>
          <mat-icon matPrefix>lock</mat-icon>
          <input matInput
                 [type]="hidePassword ? 'password' : 'text'"
                 name="password"
                 [(ngModel)]="credentials.password"
                 required
                 minlength="8"
                 #passwordCtrl="ngModel"
                 autocomplete="current-password">
          <!-- Toggle password visibility โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
          <button mat-icon-button matSuffix type="button"
                  (click)="hidePassword = !hidePassword"
                  [attr.aria-label]="hidePassword ? 'Show password' : 'Hide password'">
            <mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
          </button>
          <mat-error *ngIf="passwordCtrl.hasError('required')">Password is required</mat-error>
          <mat-error *ngIf="passwordCtrl.hasError('minlength')">
            Minimum 8 characters required
          </mat-error>
        </mat-form-field>

        <!-- mat-checkbox โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
        <mat-checkbox name="rememberMe" [(ngModel)]="rememberMe">
          Remember me
        </mat-checkbox>

        <!-- Server error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
        @if (serverError()) {
          <mat-error class="server-error">{{ serverError() }}</mat-error>
        }

        <button mat-raised-button
                color="primary"
                type="submit"
                class="full-width"
                [disabled]="loginForm.invalid || isSaving()">
          @if (isSaving()) {
            <mat-spinner diameter="20" />
          } @else {
            Sign In
          }
        </button>
      </form>

      <p class="register-link">
        Don't have an account?
        <a routerLink="/auth/register">Create one</a>
      </p>
    </div>
  `,
  styles: [`
    .full-width { width: 100%; }
    .login-card { max-width: 400px; margin: 4rem auto; padding: 2rem; }
    .server-error { display: block; margin: 0.5rem 0; }
  `],
})
export class LoginMaterialComponent {
  credentials = { email: '', password: '' };
  rememberMe  = false;
  hidePassword = true;
  isSaving    = signal(false);
  serverError = signal('');
  // ... onSubmit logic
}
Note: Angular Material’s <mat-error> inside a <mat-form-field> is shown automatically when the bound form control is in an invalid and touched state โ€” Material handles the visibility logic. You do not need @if (control.invalid && control.touched) around mat-error elements; Material’s error state machine manages that. The hasError('validatorName') method inside mat-error determines which specific message shows โ€” you can have multiple mat-error elements, one per validator failure.
Tip: Use appearance="outline" on mat-form-field for the most legible and modern Material Design 3 form style. The fill appearance is also popular. Set this globally in the Material theme to avoid repeating it on every form field. Add a global style for .full-width { width: 100%; } and apply this class to all form fields so they expand to fill the form container โ€” the default Material form field width is set by the content.
Warning: Angular Material’s mat-error elements use the *ngIf structural directive syntax (not @if) in most examples because Angular Material templates are written in a style compatible with both NgModule and standalone APIs. Using *ngIf within a standalone component’s template works fine as long as NgIf or CommonModule is imported. The new @if syntax works equally well โ€” choose consistency with the rest of your codebase.

Common Mistakes

Mistake 1 โ€” Forgetting matInput directive on inputs inside mat-form-field

โŒ Wrong โ€” <input type="text"> without matInput inside mat-form-field; no Material styling applied.

โœ… Correct โ€” always add the matInput directive: <input matInput ...>.

Mistake 2 โ€” Manually showing/hiding mat-error with @if (double-shown errors)

โŒ Wrong โ€” wrapping mat-error in @if (control.invalid && control.touched); Material already handles visibility.

โœ… Correct โ€” put mat-error directly inside mat-form-field; Angular Material manages when it appears.

🧠 Test Yourself

A mat-form-field has two mat-error elements โ€” one for required and one for email. Both conditions are true simultaneously. How many mat-error messages appear?