Differenzansicht 15-rxjs
im Vergleich zu 14-lazyloading

Zurück zur Übersicht | ← Vorherige | Demo | Quelltext auf GitHub
src/app/home-page/home-page.html CHANGED
@@ -1 +1,16 @@
1
  <h1>Welcome to the BookManager!</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <h1>Welcome to the BookManager!</h1>
2
+
3
+ <section [aria-busy]="isLoading()">
4
+ <label for="search">Search for Books</label>
5
+ <input type="search" id="search"
6
+ (input)="searchTerm$.next($event.target.value)" />
7
+ <ul>
8
+ @for (b of results(); track b.isbn) {
9
+ <li>
10
+ <a [routerLink]="['/books', 'details', b.isbn]">
11
+ {{ b.title }} / {{ b.isbn }}
12
+ </a>
13
+ </li>
14
+ }
15
+ </ul>
16
+ </section>
src/app/home-page/home-page.spec.ts CHANGED
@@ -1,26 +1,44 @@
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
  import { provideRouter } from '@angular/router';
3
  import { RouterTestingHarness } from '@angular/router/testing';
 
 
4
 
5
  import { HomePage } from './home-page';
6
  import { routes } from '../app.routes';
 
7
 
8
  describe('HomePage', () => {
9
  let component: HomePage;
10
  let fixture: ComponentFixture<HomePage>;
 
 
 
11
 
12
  beforeEach(async () => {
 
 
 
13
  await TestBed.configureTestingModule({
14
  imports: [HomePage],
15
- providers: [provideRouter(routes)]
 
 
 
 
 
 
16
  })
17
  .compileComponents();
18
 
19
  fixture = TestBed.createComponent(HomePage);
20
  component = fixture.componentInstance;
 
21
  await fixture.whenStable();
22
  });
23
 
 
 
24
  it('should create', () => {
25
  expect(component).toBeTruthy();
26
  });
@@ -32,4 +50,48 @@ describe('HomePage', () => {
32
  expect(component).toBeTruthy();
33
  expect(document.title).toBe('BookManager');
34
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  });
 
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
  import { provideRouter } from '@angular/router';
3
  import { RouterTestingHarness } from '@angular/router/testing';
4
+ import { delay, of } from 'rxjs';
5
+ import { Mock } from 'vitest';
6
 
7
  import { HomePage } from './home-page';
8
  import { routes } from '../app.routes';
9
+ import { BookStore } from '../shared/book-store';
10
 
11
  describe('HomePage', () => {
12
  let component: HomePage;
13
  let fixture: ComponentFixture<HomePage>;
14
+ let searchFn: Mock;
15
+
16
+ beforeAll(() => vi.useFakeTimers());
17
 
18
  beforeEach(async () => {
19
+ searchFn = vi.fn().mockReturnValue(
20
+ of([]).pipe(delay(100))
21
+ );
22
  await TestBed.configureTestingModule({
23
  imports: [HomePage],
24
+ providers: [
25
+ provideRouter(routes),
26
+ {
27
+ provide: BookStore,
28
+ useValue: { search: searchFn }
29
+ }
30
+ ]
31
  })
32
  .compileComponents();
33
 
34
  fixture = TestBed.createComponent(HomePage);
35
  component = fixture.componentInstance;
36
+ TestBed.tick();
37
  await fixture.whenStable();
38
  });
39
 
40
+ afterAll(() => vi.useRealTimers());
41
+
42
  it('should create', () => {
43
  expect(component).toBeTruthy();
44
  });
 
50
  expect(component).toBeTruthy();
51
  expect(document.title).toBe('BookManager');
52
  });
53
+
54
+ it('should filter search terms shorter than 3 characters', () => {
55
+ component['searchTerm$'].next('ab');
56
+ vi.advanceTimersByTime(500);
57
+
58
+ expect(searchFn).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('should search when term is 3 or more characters', () => {
62
+ component['searchTerm$'].next('abc');
63
+ vi.advanceTimersByTime(500);
64
+
65
+ expect(searchFn).toHaveBeenCalledWith('abc');
66
+ });
67
+
68
+ it('should debounce search terms', () => {
69
+ component['searchTerm$'].next('test1');
70
+ vi.advanceTimersByTime(300);
71
+ component['searchTerm$'].next('test2');
72
+ vi.advanceTimersByTime(500);
73
+
74
+ expect(searchFn).toHaveBeenCalledExactlyOnceWith('test2');
75
+ });
76
+
77
+ it('should not search for duplicate consecutive terms', () => {
78
+ component['searchTerm$'].next('test');
79
+ vi.advanceTimersByTime(500);
80
+ component['searchTerm$'].next('test2');
81
+ component['searchTerm$'].next('test');
82
+ vi.advanceTimersByTime(500);
83
+
84
+ expect(searchFn).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it('should set loading state during search', () => {
88
+ component['searchTerm$'].next('test');
89
+ vi.advanceTimersByTime(500);
90
+
91
+ expect(component['isLoading']()).toBe(true);
92
+
93
+ vi.advanceTimersByTime(100);
94
+
95
+ expect(component['isLoading']()).toBe(false);
96
+ });
97
  });
src/app/home-page/home-page.ts CHANGED
@@ -1,11 +1,31 @@
1
- import { Component } from '@angular/core';
 
 
 
 
 
2
 
3
  @Component({
4
  selector: 'app-home-page',
5
- imports: [],
6
  templateUrl: './home-page.html',
7
  styleUrl: './home-page.css'
8
  })
9
  export class HomePage {
 
 
 
 
10
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
 
1
+ import { Component, inject, signal } from '@angular/core';
2
+ import { RouterLink } from '@angular/router';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+ import { filter, debounceTime, distinctUntilChanged, switchMap, tap, Subject } from 'rxjs';
5
+
6
+ import { BookStore } from '../shared/book-store';
7
 
8
  @Component({
9
  selector: 'app-home-page',
10
+ imports: [RouterLink],
11
  templateUrl: './home-page.html',
12
  styleUrl: './home-page.css'
13
  })
14
  export class HomePage {
15
+ #bookStore = inject(BookStore);
16
+
17
+ protected searchTerm$ = new Subject<string>();
18
+ protected isLoading = signal(false);
19
 
20
+ protected results = toSignal(
21
+ this.searchTerm$.pipe(
22
+ filter(term => term.length >= 3),
23
+ debounceTime(500),
24
+ distinctUntilChanged(),
25
+ tap(() => this.isLoading.set(true)),
26
+ switchMap(term => this.#bookStore.search(term)),
27
+ tap(() => this.isLoading.set(false)),
28
+ ),
29
+ { initialValue: [] }
30
+ );
31
  }
src/app/shared/book-store.ts CHANGED
@@ -36,4 +36,11 @@ export class BookStore {
36
  this.#http.post<Book>(`${this.#apiUrl}/books`, book)
37
  );
38
  }
 
 
 
 
 
 
 
39
  }
 
36
  this.#http.post<Book>(`${this.#apiUrl}/books`, book)
37
  );
38
  }
39
+
40
+ search(searchTerm: string): Observable<Book[]> {
41
+ return this.#http.get<Book[]>(
42
+ `${this.#apiUrl}/books`,
43
+ { params: { filter: searchTerm } }
44
+ );
45
+ }
46
  }