From 38f4ea1ea89d674fb4f4db1fe67953eed26d57fb Mon Sep 17 00:00:00 2001 From: Ben Hong Date: Wed, 19 Nov 2025 11:16:06 -0500 Subject: [PATCH] docs: add signal forms validation guide --- adev/src/app/routing/sub-navigation-data.ts | 5 + .../signal-forms/src/login-simple/index.html | 14 - .../signal-forms/src/login-simple/main.ts | 5 - .../src/login-validation-complete/app/app.css | 70 ++ .../login-validation-complete/app/app.html | 33 + .../src/login-validation-complete/app/app.ts | 38 ++ .../src/login-validation/app/app.html | 4 +- .../src/login-validation/app/app.ts | 4 +- .../src/login-validation/index.html | 14 - .../signal-forms/src/login-validation/main.ts | 5 - .../content/guide/forms/signals/validation.md | 629 ++++++++++++++++++ 11 files changed, 778 insertions(+), 43 deletions(-) delete mode 100644 adev/src/content/examples/signal-forms/src/login-simple/index.html delete mode 100644 adev/src/content/examples/signal-forms/src/login-simple/main.ts create mode 100644 adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.css create mode 100644 adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.html create mode 100644 adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.ts delete mode 100644 adev/src/content/examples/signal-forms/src/login-validation/index.html delete mode 100644 adev/src/content/examples/signal-forms/src/login-validation/main.ts create mode 100644 adev/src/content/guide/forms/signals/validation.md diff --git a/adev/src/app/routing/sub-navigation-data.ts b/adev/src/app/routing/sub-navigation-data.ts index 2c3e78cd652..b5e3435dc58 100644 --- a/adev/src/app/routing/sub-navigation-data.ts +++ b/adev/src/app/routing/sub-navigation-data.ts @@ -447,6 +447,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/forms/signals/field-state-management', contentPath: 'guide/forms/signals/field-state-management', }, + { + label: 'Validation', + path: 'guide/forms/signals/validation', + contentPath: 'guide/forms/signals/validation', + }, { label: 'Comparison with other form systems', path: 'guide/forms/signals/comparison', diff --git a/adev/src/content/examples/signal-forms/src/login-simple/index.html b/adev/src/content/examples/signal-forms/src/login-simple/index.html deleted file mode 100644 index 34474578c22..00000000000 --- a/adev/src/content/examples/signal-forms/src/login-simple/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Signal Forms - Login Example - - - - - - - - diff --git a/adev/src/content/examples/signal-forms/src/login-simple/main.ts b/adev/src/content/examples/signal-forms/src/login-simple/main.ts deleted file mode 100644 index 917e1f40300..00000000000 --- a/adev/src/content/examples/signal-forms/src/login-simple/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {bootstrapApplication} from '@angular/platform-browser'; - -import {App} from './app/app'; - -bootstrapApplication(App); diff --git a/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.css b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.css new file mode 100644 index 00000000000..08907951c7e --- /dev/null +++ b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.css @@ -0,0 +1,70 @@ +form { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 400px; + padding: 1rem; + font-family: + Inter, + system-ui, + -apple-system, + sans-serif; +} + +div { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +label { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-weight: 500; +} + +input { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; + font-family: inherit; +} + +input:focus { + outline: none; + border-color: #4285f4; +} + +button { + padding: 0.75rem 1.5rem; + background-color: #4285f4; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-family: inherit; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #357ae8; +} + +button:active { + background-color: #2a65c8; +} + +.error-list { + color: red; + font-size: 0.875rem; + margin: 0.25rem 0 0 0; + padding-left: 0; + list-style-position: inside; +} + +.error-list li { + margin: 0; +} diff --git a/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.html b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.html new file mode 100644 index 00000000000..9ca4d2ac979 --- /dev/null +++ b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.html @@ -0,0 +1,33 @@ +
+
+ + + @if (loginForm.email().touched() && loginForm.email().invalid()) { +
    + @for (error of loginForm.email().errors(); track error) { +
  • {{ error.message }}
  • + } +
+ } +
+ +
+ + + @if (loginForm.password().touched() && loginForm.password().invalid()) { +
    + @for (error of loginForm.password().errors(); track error) { +
  • {{ error.message }}
  • + } +
+ } +
+ + +
diff --git a/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.ts b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.ts new file mode 100644 index 00000000000..2614be573be --- /dev/null +++ b/adev/src/content/examples/signal-forms/src/login-validation-complete/app/app.ts @@ -0,0 +1,38 @@ +import {Component, signal, ChangeDetectionStrategy} from '@angular/core'; +import {form, Field, required, email, submit} from '@angular/forms/signals'; + +interface LoginData { + email: string; + password: string; +} + +@Component({ + selector: 'app-root', + templateUrl: 'app.html', + styleUrl: 'app.css', + imports: [Field], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + loginModel = signal({ + email: '', + password: '', + }); + + loginForm = form(this.loginModel, (schemaPath) => { + required(schemaPath.email, {message: 'Email is required'}); + email(schemaPath.email, {message: 'Enter a valid email address'}); + + required(schemaPath.password, {message: 'Password is required'}); + }); + + onSubmit(event: Event) { + event.preventDefault(); + submit(this.loginForm, async () => { + const credentials = this.loginModel(); + // In a real app, this would be async: + // await this.authService.login(credentials); + console.log('Logging in with:', credentials); + }); + } +} diff --git a/adev/src/content/examples/signal-forms/src/login-validation/app/app.html b/adev/src/content/examples/signal-forms/src/login-validation/app/app.html index 07f390c9cc6..73ec4635295 100644 --- a/adev/src/content/examples/signal-forms/src/login-validation/app/app.html +++ b/adev/src/content/examples/signal-forms/src/login-validation/app/app.html @@ -5,7 +5,7 @@ - @if (loginForm.email().invalid()) { + @if (loginForm.email().touched() && loginForm.email().invalid()) {