
// registration.model.ts
export interface RegistrationData {
email: string;
password: string;
confirmPassword: string;
acceptedTerms: boolean;
}
This interface mirrors what would typically be sent to a back-end API. There is no duplication of state, no separate “form value” object, and no mapping required at submission time.
Creating the signal-backed form
The form itself is created in the component using a writable signal as the source of truth. The form() function attaches form semantics validation, field state, and submission to that signal.
// registration.component.ts
import { CommonModule } from "@angular/common";
import { Component, signal } from "@angular/core";
import {
email,
form,
FormField,
required,
submit,
} from "@angular/forms/signals";
import { RegistrationData } from "./registration.model";
@Component({
selector: "app-registration",
imports: [FormField, CommonModule],
templateUrl: "./registration.html",
styleUrl: "./registration.css",
})
export class Registration {
readonly model = signal({
email: "",
password: "",
confirmPassword: "",
acceptedTerms: false,
});
readonly registrationForm = form(this.model, (schema) => {
required(schema.email, { message: "Email is required" });
email(schema.email, { message: "Enter a valid email address" });
required(schema.password, { message: "Password is required" });
required(schema.confirmPassword, {
message: "Please confirm your password",
});
required(schema.acceptedTerms, {
message: "You must accept the terms to continue",
});
});
async onSubmit(event?: Event) {
event?.preventDefault();
await submit(this.registrationForm, (value) => {
console.log(value());
// Mock Server Call
return Promise.resolve([
{
kind: "EmailAlreadyExists",
field: this.registrationForm.email,
error: { kind: "server", message: "Email already taken" },
},
]);
});
}
}
Several design decisions are worth noting.

