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,48 @@
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 +54,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 bookStoreSearchMock: Mock;
15
+
16
+ beforeAll(() => {
17
+ vi.useFakeTimers();
18
+ });
19
 
20
  beforeEach(async () => {
21
+ bookStoreSearchMock = vi.fn().mockReturnValue(
22
+ of([]).pipe(delay(100))
23
+ );
24
  await TestBed.configureTestingModule({
25
  imports: [HomePage],
26
+ providers: [
27
+ provideRouter(routes),
28
+ {
29
+ provide: BookStore,
30
+ useValue: { search: bookStoreSearchMock }
31
+ }
32
+ ]
33
  })
34
  .compileComponents();
35
 
36
  fixture = TestBed.createComponent(HomePage);
37
  component = fixture.componentInstance;
38
+ TestBed.tick();
39
  await fixture.whenStable();
40
  });
41
 
42
+ afterAll(() => {
43
+ vi.useRealTimers();
44
+ });
45
+
46
  it('should create', () => {
47
  expect(component).toBeTruthy();
48
  });
 
54
  expect(component).toBeTruthy();
55
  expect(document.title).toBe('BookManager');
56
  });
57
+
58
+ it('should filter search terms shorter than 3 characters', () => {
59
+ component['searchTerm$'].next('ab');
60
+ vi.advanceTimersByTime(500);
61
+
62
+ expect(bookStoreSearchMock).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('should search when term is 3 or more characters', () => {
66
+ component['searchTerm$'].next('abc');
67
+ vi.advanceTimersByTime(500);
68
+
69
+ expect(bookStoreSearchMock).toHaveBeenCalledWith('abc');
70
+ });
71
+
72
+ it('should debounce search terms', () => {
73
+ component['searchTerm$'].next('test1');
74
+ vi.advanceTimersByTime(300);
75
+ component['searchTerm$'].next('test2');
76
+ vi.advanceTimersByTime(500);
77
+
78
+ expect(bookStoreSearchMock).toHaveBeenCalledExactlyOnceWith('test2');
79
+ });
80
+
81
+ it('should not search for duplicate consecutive terms', () => {
82
+ component['searchTerm$'].next('test');
83
+ vi.advanceTimersByTime(500);
84
+ component['searchTerm$'].next('test2');
85
+ component['searchTerm$'].next('test');
86
+ vi.advanceTimersByTime(500);
87
+
88
+ expect(bookStoreSearchMock).toHaveBeenCalledTimes(1);
89
+ });
90
+
91
+ it('should set loading state during search', () => {
92
+ component['searchTerm$'].next('test');
93
+ vi.advanceTimersByTime(500);
94
+
95
+ expect(component['isLoading']()).toBe(true);
96
+
97
+ vi.advanceTimersByTime(100);
98
+
99
+ expect(component['isLoading']()).toBe(false);
100
+ });
101
  });
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
  }