Differenzansicht 12a-validation-sf
im Vergleich zu 12-validation

← Zurück zur Übersicht | Demo | Quelltext auf GitHub
package.json CHANGED
@@ -28,6 +28,7 @@
28
  "@angular/forms": "^20.1.0",
29
  "@angular/platform-browser": "^20.1.0",
30
  "@angular/router": "^20.1.0",
 
31
  "rxjs": "~7.8.0",
32
  "tslib": "^2.3.0",
33
  "zone.js": "~0.15.0"
@@ -48,4 +49,4 @@
48
  "typescript": "~5.8.2",
49
  "typescript-eslint": "8.34.1"
50
  }
51
- }
 
28
  "@angular/forms": "^20.1.0",
29
  "@angular/platform-browser": "^20.1.0",
30
  "@angular/router": "^20.1.0",
31
+ "@standard-schema/spec": "^1.0.0",
32
  "rxjs": "~7.8.0",
33
  "tslib": "^2.3.0",
34
  "zone.js": "~0.15.0"
 
49
  "typescript": "~5.8.2",
50
  "typescript-eslint": "8.34.1"
51
  }
52
+ }
src/app/books-admin/book-form/book-form.html CHANGED
@@ -1,12 +1,10 @@
1
- @let c = bookForm.controls;
2
-
3
- <form [formGroup]="bookForm" (ngSubmit)="submitForm()">
4
- @let titleInvalid = isInvalid(c.title);
5
  <label for="title">Title</label>
6
  <input
7
  type="text"
8
  id="title"
9
- [formControl]="c.title"
10
  [attr.aria-describedby]="titleInvalid ? 'title-error' : null"
11
  [attr.aria-invalid]="titleInvalid"
12
  />
@@ -15,14 +13,14 @@
15
  }
16
 
17
  <label for="subtitle">Subtitle</label>
18
- <input type="text" id="subtitle" [formControl]="c.subtitle" />
19
 
20
- @let isbnInvalid = isInvalid(c.isbn);
21
  <label for="isbn">ISBN</label>
22
  <input
23
  type="text"
24
  id="isbn"
25
- [formControl]="c.isbn"
26
  [attr.aria-describedby]="isbnInvalid ? 'isbn-error' : null"
27
  [attr.aria-invalid]="isbnInvalid"
28
  />
@@ -33,22 +31,52 @@
33
  <fieldset>
34
  <legend>Authors</legend>
35
  <button type="button" (click)="addAuthorControl()">Add Author</button>
 
36
  <div role="group">
37
- @for (authorControl of c.authors.controls; track $index) {
38
  <input
39
  type="text"
40
  [attr.aria-label]="'Author ' + $index + 1"
41
- [formControl]="authorControl"
 
 
42
  />
43
  }
 
 
 
44
  </div>
 
45
  </fieldset>
46
 
 
47
  <label for="description">Description</label>
48
- <textarea id="description" [formControl]="c.description"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
 
50
  <label for="imageUrl">Thumbnail URL</label>
51
- <input type="url" id="imageUrl" [formControl]="c.imageUrl" />
 
 
 
 
 
 
 
 
52
 
53
- <button type="submit" [disabled]="bookForm.invalid">Save</button>
54
  </form>
 
1
+ <form (submit)="submitForm($event)">
2
+ @let titleInvalid = isInvalid(f.title());
 
 
3
  <label for="title">Title</label>
4
  <input
5
  type="text"
6
  id="title"
7
+ [control]="f.title"
8
  [attr.aria-describedby]="titleInvalid ? 'title-error' : null"
9
  [attr.aria-invalid]="titleInvalid"
10
  />
 
13
  }
14
 
15
  <label for="subtitle">Subtitle</label>
16
+ <input type="text" id="subtitle" [control]="f.subtitle!" />
17
 
18
+ @let isbnInvalid = isInvalid(f.isbn());
19
  <label for="isbn">ISBN</label>
20
  <input
21
  type="text"
22
  id="isbn"
23
+ [control]="f.isbn"
24
  [attr.aria-describedby]="isbnInvalid ? 'isbn-error' : null"
25
  [attr.aria-invalid]="isbnInvalid"
26
  />
 
31
  <fieldset>
32
  <legend>Authors</legend>
33
  <button type="button" (click)="addAuthorControl()">Add Author</button>
34
+ @let authorsInvalid = isInvalid(f.authors());
35
  <div role="group">
36
+ @for (authorControl of f.authors; track $index) {
37
  <input
38
  type="text"
39
  [attr.aria-label]="'Author ' + $index + 1"
40
+ [control]="authorControl"
41
+ [attr.aria-describedby]="authorsInvalid ? 'authors-error' : null"
42
+ [attr.aria-invalid]="authorsInvalid"
43
  />
44
  }
45
+ @if (authorsInvalid) {
46
+ <small id="authors-error">At least one author must be added.</small>
47
+ }
48
  </div>
49
+
50
  </fieldset>
51
 
52
+ @let descriptionInvalid = isInvalid(f.description());
53
  <label for="description">Description</label>
54
+ <input
55
+ id="description"
56
+ [control]="f.description"
57
+ [attr.aria-describedby]="descriptionInvalid ? 'description-error' : null"
58
+ [attr.aria-invalid]="descriptionInvalid" />
59
+ <!-- <textarea
60
+ id="description"
61
+ [control]="f.description"
62
+ [attr.aria-describedby]="descriptionInvalid ? 'description-error' : null"
63
+ [attr.aria-invalid]="descriptionInvalid">
64
+ </textarea> -->
65
+ @if (descriptionInvalid) {
66
+ <small id="description-error">The description is invalid.</small>
67
+ }
68
 
69
+ @let imageUrlInvalid = isInvalid(f.imageUrl());
70
  <label for="imageUrl">Thumbnail URL</label>
71
+ <input
72
+ type="url"
73
+ id="imageUrl"
74
+ [control]="f.imageUrl"
75
+ [attr.aria-describedby]="imageUrlInvalid ? 'description-error' : null"
76
+ [attr.aria-invalid]="imageUrlInvalid" />
77
+ @if (imageUrlInvalid) {
78
+ <small id="image-url-error">The URL is invalid.</small>
79
+ }
80
 
81
+ <button type="submit" [disabled]="f().invalid()" [attr.aria-busy]="f().submitting()" >Save</button>
82
  </form>
src/app/books-admin/book-form/book-form.ts CHANGED
@@ -1,61 +1,65 @@
1
- import { Component, output } from '@angular/core';
2
- import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3
 
4
  import { Book } from '../../shared/book';
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  @Component({
7
  selector: 'app-book-form',
8
- imports: [ReactiveFormsModule],
9
  templateUrl: './book-form.html',
10
  styleUrl: './book-form.scss'
11
  })
12
  export class BookForm {
13
  readonly submitBook = output<Book>();
14
 
15
- protected bookForm = new FormGroup({
16
- isbn: new FormControl('', {
17
- nonNullable: true,
18
- validators: [
19
- Validators.required,
20
- Validators.minLength(13),
21
- Validators.maxLength(13),
22
- ]
23
- }),
24
- title: new FormControl('', {
25
- nonNullable: true,
26
- validators: Validators.required,
27
- }),
28
- subtitle: new FormControl('', { nonNullable: true }),
29
- description: new FormControl('', { nonNullable: true }),
30
- authors: new FormArray([
31
- new FormControl('', { nonNullable: true })
32
- ]),
33
- imageUrl: new FormControl('', { nonNullable: true })
34
- });
35
 
36
  addAuthorControl() {
37
- this.bookForm.controls.authors.push(
38
- new FormControl('', { nonNullable: true })
39
- );
40
  }
41
 
42
- isInvalid(control: FormControl) {
43
- if (!control.touched) {
44
  return null;
45
  }
46
- return control.invalid && control.touched;
47
  }
48
 
49
- submitForm() {
50
- const formValue = this.bookForm.getRawValue();
51
- const authors = formValue.authors.filter(author => !!author);
52
 
53
- const newBook: Book = {
54
- ...formValue,
55
- authors,
56
- createdAt: new Date().toISOString()
57
- };
58
 
59
- this.submitBook.emit(newBook);
 
 
 
 
 
 
60
  }
61
  }
 
1
+ import { Component, output, signal } from '@angular/core';
2
+ import { Control, FieldState, form, maxLength, minLength, required, schema, submit, validate, ValidationError } from '@angular/forms/signal-experimental';
3
 
4
  import { Book } from '../../shared/book';
5
 
6
+ export const formSchema = schema<Book>((fieldPath) => {
7
+ required(fieldPath.title);
8
+ required(fieldPath.isbn);
9
+ minLength(fieldPath.isbn, 13);
10
+ maxLength(fieldPath.isbn, 13);
11
+ validate(fieldPath.authors, ({ value }) =>
12
+ !value().some((a) => a)
13
+ ? ValidationError.custom({ kind: 'atLeastOneAuthor' })
14
+ : undefined
15
+ );
16
+ required(fieldPath.description);
17
+ required(fieldPath.imageUrl);
18
+ })
19
+
20
  @Component({
21
  selector: 'app-book-form',
22
+ imports: [Control],
23
  templateUrl: './book-form.html',
24
  styleUrl: './book-form.scss'
25
  })
26
  export class BookForm {
27
  readonly submitBook = output<Book>();
28
 
29
+ protected readonly f = form(signal<Book>({
30
+ isbn: "",
31
+ title: "",
32
+ subtitle: "",
33
+ authors: [""],
34
+ description: "",
35
+ imageUrl: "",
36
+ createdAt: new Date().toISOString(),
37
+ }), formSchema);
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  addAuthorControl() {
40
+ this.f.authors().value.update((authors) => [...authors, '']);
 
 
41
  }
42
 
43
+ isInvalid(field: FieldState<string | string[]>) {
44
+ if (!field.touched()) {
45
  return null;
46
  }
47
+ return !field.valid() && field.touched();
48
  }
49
 
50
+ async submitForm(e: Event) {
51
+ e?.preventDefault();
 
52
 
53
+ await submit(this.f, async (form) => {
54
+ const formValue = this.f().value();
55
+ const authors = this.f.authors().value().filter(author => !!author);
 
 
56
 
57
+ const newBook: Book = {
58
+ ...formValue,
59
+ authors,
60
+ createdAt: new Date().toISOString()
61
+ };
62
+ this.submitBook.emit(newBook);
63
+ });
64
  }
65
  }
src/app/books-portal/book-details-page/book-details-page.html CHANGED
@@ -4,11 +4,10 @@
4
  }
5
 
6
  @if (book.isLoading()) {
7
- <p aria-busy="true">Loading ...</p>
8
  }
9
 
10
- @if (book.hasValue()) {
11
- @let b = book.value();
12
  <article>
13
  <header>
14
  <h1>{{ b.title }}</h1>
 
4
  }
5
 
6
  @if (book.isLoading()) {
7
+ <p aria-busy="true">Loading...</p>
8
  }
9
 
10
+ @if (book.value(); as b) {
 
11
  <article>
12
  <header>
13
  <h1>{{ b.title }}</h1>
src/app/books-portal/books-overview-page/books-overview-page.ts CHANGED
@@ -19,10 +19,6 @@ export class BooksOverviewPage {
19
  protected likedBooks = signal<Book[]>([]);
20
 
21
  protected filteredBooks = computed(() => {
22
- if (!this.books.hasValue()) {
23
- return [];
24
- }
25
-
26
  if (!this.searchTerm()) {
27
  return this.books.value();
28
  }
 
19
  protected likedBooks = signal<Book[]>([]);
20
 
21
  protected filteredBooks = computed(() => {
 
 
 
 
22
  if (!this.searchTerm()) {
23
  return this.books.value();
24
  }
tsconfig.json CHANGED
@@ -3,6 +3,11 @@
3
  {
4
  "compileOnSave": false,
5
  "compilerOptions": {
 
 
 
 
 
6
  "strict": true,
7
  "noImplicitOverride": true,
8
  "noPropertyAccessFromIndexSignature": true,
 
3
  {
4
  "compileOnSave": false,
5
  "compilerOptions": {
6
+ "baseUrl": ".",
7
+ "paths": {
8
+ "@angular/forms/signal-experimental": ["./experimental"],
9
+ "@angular/forms/standard_schema": ["./experimental/src/api/validators/standard_schema"]
10
+ },
11
  "strict": true,
12
  "noImplicitOverride": true,
13
  "noPropertyAccessFromIndexSignature": true,