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

← Zurück zur Übersicht | Demo | Quelltext auf GitHub
package.json CHANGED
@@ -30,7 +30,6 @@
30
  "@angular/forms": "^20.1.0",
31
  "@angular/platform-browser": "^20.1.0",
32
  "@angular/router": "^20.1.0",
33
- "@standard-schema/spec": "^1.0.0",
34
  "rxjs": "~7.8.0",
35
  "tslib": "^2.3.0",
36
  "zone.js": "~0.15.0"
@@ -51,4 +50,4 @@
51
  "typescript": "~5.8.2",
52
  "typescript-eslint": "8.34.1"
53
  }
54
- }
 
30
  "@angular/forms": "^20.1.0",
31
  "@angular/platform-browser": "^20.1.0",
32
  "@angular/router": "^20.1.0",
 
33
  "rxjs": "~7.8.0",
34
  "tslib": "^2.3.0",
35
  "zone.js": "~0.15.0"
 
50
  "typescript": "~5.8.2",
51
  "typescript-eslint": "8.34.1"
52
  }
53
+ }
src/app/books-admin/book-form/book-form.html CHANGED
@@ -1,10 +1,12 @@
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,14 @@
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,47 +33,22 @@
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
- <textarea
55
- id="description"
56
- [control]="f.description"
57
- [attr.aria-describedby]="descriptionInvalid ? 'description-error' : null"
58
- [attr.aria-invalid]="descriptionInvalid">
59
- </textarea>
60
- @if (descriptionInvalid) {
61
- <small id="description-error">The description is invalid.</small>
62
- }
63
 
64
- @let imageUrlInvalid = isInvalid(f.imageUrl());
65
  <label for="imageUrl">Thumbnail URL</label>
66
- <input
67
- type="url"
68
- id="imageUrl"
69
- [control]="f.imageUrl"
70
- [attr.aria-describedby]="imageUrlInvalid ? 'description-error' : null"
71
- [attr.aria-invalid]="imageUrlInvalid" />
72
- @if (imageUrlInvalid) {
73
- <small id="image-url-error">The URL is invalid.</small>
74
- }
75
 
76
- <button type="submit" [disabled]="f().invalid()" [attr.aria-busy]="f().submitting()" >Save</button>
77
  </form>
 
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
  }
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
  <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>
src/app/books-admin/book-form/book-form.ts CHANGED
@@ -1,65 +1,61 @@
1
- import { Component, output, signal } from '@angular/core';
2
- import { Control, customError, FieldState, form, maxLength, minLength, required, schema, submit, validate } from '@angular/forms/signals';
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
- ? customError({ 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
  }
 
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
  }
src/app/books-portal/book-details-page/book-details-page.html CHANGED
@@ -4,10 +4,11 @@
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>
 
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>
src/app/books-portal/books-overview-page/books-overview-page.html CHANGED
@@ -21,11 +21,12 @@
21
  type="search"
22
  #searchInput
23
  (input)="this.searchTerm.set(searchInput.value)"
 
24
  placeholder="Search"
25
  aria-label="Search"
26
  />
27
 
28
- @for (b of filteredBooks(); track b.isbn) {
29
  <app-book-card [book]="b" (like)="addLikedBook($event)" />
30
  }
31
  </div>
 
21
  type="search"
22
  #searchInput
23
  (input)="this.searchTerm.set(searchInput.value)"
24
+ [value]="this.searchTerm()"
25
  placeholder="Search"
26
  aria-label="Search"
27
  />
28
 
29
+ @for (b of books.value(); track b.isbn) {
30
  <app-book-card [book]="b" (like)="addLikedBook($event)" />
31
  }
32
  </div>
src/app/books-portal/books-overview-page/books-overview-page.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Component, computed, inject, signal } from '@angular/core';
 
2
 
3
  import { Book } from '../../shared/book';
4
  import { BookCard } from '../book-card/book-card';
@@ -12,20 +13,23 @@ import { BookStore } from '../../shared/book-store';
12
  })
13
  export class BooksOverviewPage {
14
  #bookStore = inject(BookStore);
 
15
 
16
- protected searchTerm = signal('');
 
17
 
18
- protected books = this.#bookStore.getAll();
19
  protected likedBooks = signal<Book[]>([]);
20
 
21
- protected filteredBooks = computed(() => {
22
- if (!this.searchTerm()) {
23
- return this.books.value();
24
- }
25
-
26
- const term = this.searchTerm().toLowerCase();
27
- return this.books.value().filter((b) => b.title.toLowerCase().includes(term));
28
- });
 
29
 
30
  addLikedBook(newLikedBook: Book) {
31
  const foundBook = this.likedBooks().find(
 
1
+ import { Component, effect, inject, input, linkedSignal, signal } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
 
4
  import { Book } from '../../shared/book';
5
  import { BookCard } from '../book-card/book-card';
 
13
  })
14
  export class BooksOverviewPage {
15
  #bookStore = inject(BookStore);
16
+ #router = inject(Router);
17
 
18
+ readonly search = input<string>();
19
+ protected searchTerm = linkedSignal(() => this.search() || '');
20
 
21
+ protected books = this.#bookStore.getAll(this.searchTerm);
22
  protected likedBooks = signal<Book[]>([]);
23
 
24
+ constructor() {
25
+ effect(() => {
26
+ this.#router.navigate([], {
27
+ queryParams: {
28
+ search: this.searchTerm() || null
29
+ }
30
+ });
31
+ });
32
+ }
33
 
34
  addLikedBook(newLikedBook: Book) {
35
  const foundBook = this.likedBooks().find(
src/app/shared/book-store.ts CHANGED
@@ -11,9 +11,12 @@ export class BookStore {
11
  #http = inject(HttpClient);
12
  #apiUrl = 'https://api6.angular-buch.com';
13
 
14
- getAll(): HttpResourceRef<Book[]> {
15
  return httpResource<Book[]>(
16
- () => `${this.#apiUrl}/books`,
 
 
 
17
  { defaultValue: [] }
18
  );
19
  }
 
11
  #http = inject(HttpClient);
12
  #apiUrl = 'https://api6.angular-buch.com';
13
 
14
+ getAll(searchTerm: Signal<string>): HttpResourceRef<Book[]> {
15
  return httpResource<Book[]>(
16
+ () => ({
17
+ url: `${this.#apiUrl}/books`,
18
+ params: { search: searchTerm() }
19
+ }),
20
  { defaultValue: [] }
21
  );
22
  }
tsconfig.json CHANGED
@@ -3,11 +3,6 @@
3
  {
4
  "compileOnSave": false,
5
  "compilerOptions": {
6
- "baseUrl": ".",
7
- "paths": {
8
- "@angular/forms/signals": ["./experimental"],
9
- "@angular/forms/standard_schema": ["./experimental/src/api/validators/standard_schema"]
10
- },
11
  "strict": true,
12
  "noImplicitOverride": true,
13
  "noPropertyAccessFromIndexSignature": true,
 
3
  {
4
  "compileOnSave": false,
5
  "compilerOptions": {
 
 
 
 
 
6
  "strict": true,
7
  "noImplicitOverride": true,
8
  "noPropertyAccessFromIndexSignature": true,