โ† Back to list

๐Ÿ’Ž Angular Signal Forms: improved form submission

Did you know that Signal Forms submission got a lot more convenient in Angular 21.2?

Wojciech Trawiล„ski in JavaScript everyday ยท 2026-02-26 18:25 ยท 98 claps ยท 2.6 min read
#angular #web-development #javascript #typescript #angular-signal-forms
Open on Medium โ†—
Wiki topics: ๐ŸŒ ยท Web Development

๐Ÿ’Ž Angular Signal Forms: improved form submission

Did you know that Signal Forms submission got a lot more convenient in Angular 21.2?

Letโ€™s look at a simple LoginForm component using Angular Signal Forms (still experimental package):

import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login-form',
  imports: [FormField],
  template: `
    <form (submit)="login($event)" novalidate>
      <div class="form-control">
        <label for="email">Email</label>
        <input type="email" id="email" [formField]="loginForm.email" />
      </div>

      <div class="form-control">
        <label for="password">Password</label>
        <input type="password" id="password" [formField]="loginForm.password" />
      </div>

      <div class="action-btns">
        <button type="submit">Login</button>
      </div>
    </form>
  `,
})
export class LoginForm {
  private readonly loginModel = signal<LoginData>({ email: '', password: '' });
  protected readonly loginForm = form(this.loginModel, (schemaPath) => {
    required(schemaPath.email);
    email(schemaPath.email);

    required(schemaPath.password);
  });

  protected login(event: SubmitEvent) {
    event.preventDefault();

    if (this.loginForm().invalid()) {
      const firstError = this.loginForm().errorSummary()[0];

      firstError.fieldTree().focusBoundControl();
      return;
    }

    submit(this.loginForm, async (form) => {
      console.log('Logging in with data:', form().value());
    });
  }
}

It works, but thereโ€™s some boilerplate you need to remember:

  • add the novalidate attribute to prevent built-in browser validation
  • call event.preventDefault() in the submit handler to prevent page reload
  • check if the form is invalid and handle that case

Angular 21.2 improves all of this. First, you can define actions for both the valid and invalid scenarios in a single place โ€” as the second argument of submit:

import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login-form',
  imports: [FormField],
  template: `...`,
})
export class LoginForm {
  private readonly loginModel = signal<LoginData>({ email: '', password: '' });
  protected readonly loginForm = form(this.loginModel, (schemaPath) => {
    required(schemaPath.email);
    email(schemaPath.email);

    required(schemaPath.password);
  });

  protected login(event: SubmitEvent) {
    event.preventDefault();

    submit(this.loginForm, {
     // when form is valid
      action: async (form) => {
        console.log('Logging in with data:', form().value());
      },
      // when form is invalid
      onInvalid: (form) => {
        const firstError = form().errorSummary()[0];

        firstError.fieldTree().focusBoundControl();
      },
    });
  }
}

Even better, you can define the submission logic directly when defining the form:

import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login-form',
  imports: [FormField],
  template: `...`,
})
export class LoginForm {
  private readonly loginModel = signal<LoginData>({ email: '', password: '' });
  protected readonly loginForm = form(
    this.loginModel,
    (schemaPath) => {
      required(schemaPath.email);
      email(schemaPath.email);

      required(schemaPath.password);
    },
    {
     // submission logic
      submission: {
        action: async (form) => {
          console.log('Logging in with data:', form().value());
        },
        onInvalid: (form) => {
          const firstError = form().errorSummary()[0];

          firstError.fieldTree().focusBoundControl();
        },
      },
    },
  );

  protected login(event: SubmitEvent) {
    event.preventDefault();

    submit(this.loginForm);
  }
}

Finally, the new FormRoot directive handles calling event.preventDefault() and adding novalidate for you. It listens to the submit event and invokes the actions defined in the submission property of the form definition:

import { Component, signal } from '@angular/core';
import { email, form, FormField, FormRoot, required, submit } from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login-form',
  imports: [FormField, FormRoot],
  template: `
   <!-- FormRoot directive -->
    <form [formRoot]="loginForm">
      <div class="form-control">
        <label for="email">Email</label>
        <input type="email" id="email" [formField]="loginForm.email" />
      </div>

      <div class="form-control">
        <label for="password">Password</label>
        <input type="password" id="password" [formField]="loginForm.password" />
      </div>

      <div class="action-btns">
        <button type="submit">Login</button>
      </div>
    </form>
  `,
})
export class LoginForm {
  private readonly loginModel = signal<LoginData>({ email: '', password: '' });
  protected readonly loginForm = form(
    this.loginModel,
    (schemaPath) => {
      required(schemaPath.email);
      email(schemaPath.email);

      required(schemaPath.password);
    },
    {
      submission: {
        action: async (form) => {
          console.log('Logging in with data:', form().value());
        },
        onInvalid: (form) => {
          const firstError = form().errorSummary()[0];

          firstError.fieldTree().focusBoundControl();
        },
      },
    },
  );
}

Angular 21.2 brings some really nice improvements to Signal Forms submission โ€” a solid DX boost in a minor update.

I hope you liked the โ€œgolden nuggetโ€ ๐Ÿ’Ž , thanks for reading! ๐Ÿ™‚


๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
post_id
a44c7e6f2b8d
slug
angular-signal-forms-improved-form-submission-a44c7e6f2b8d
url
https://medium.com/javascript-everyday/angular-signal-forms-improved-form-submission-a44c7e6f2b8d
canonical_url
https://medium.com/javascript-everyday/angular-signal-forms-improved-form-submission-a44c7e6f2b8d
author_url
https://medium.com/@wojtrawi
status
ok
fetched_at
2026-06-14 11:28:49