Differenzansicht 08-rxjs
im Vergleich zu 07-http

← Zurück zur Ãœbersicht | Demo | Quelltext auf GitHub
src/app/app.module.ts CHANGED
@@ -6,11 +6,13 @@ import { AppRoutingModule } from './app-routing.module';
6
  import { AppComponent } from './app.component';
7
  import { BooksModule } from './books/books.module';
8
  import { HomeComponent } from './home/home.component';
 
9
 
10
  @NgModule({
11
  declarations: [
12
  AppComponent,
13
  HomeComponent,
 
14
  ],
15
  imports: [
16
  BrowserModule,
6
  import { AppComponent } from './app.component';
7
  import { BooksModule } from './books/books.module';
8
  import { HomeComponent } from './home/home.component';
9
+ import { SearchComponent } from './search/search.component';
10
 
11
  @NgModule({
12
  declarations: [
13
  AppComponent,
14
  HomeComponent,
15
+ SearchComponent,
16
  ],
17
  imports: [
18
  BrowserModule,
src/app/books/book-details/book-details.component.html CHANGED
@@ -1,4 +1,4 @@
1
- <div class="details" *ngIf="book">
2
  <h1>{{ book.title }}</h1>
3
  <p role="doc-subtitle" *ngIf="book.subtitle">{{ book.subtitle }}</p>
4
  <div class="header">
1
+ <div class="details" *ngIf="book$ | async as book">
2
  <h1>{{ book.title }}</h1>
3
  <p role="doc-subtitle" *ngIf="book.subtitle">{{ book.subtitle }}</p>
4
  <div class="header">
src/app/books/book-details/book-details.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BookDetailsComponent } from './book-details.component';
4
+
5
+ describe('BookDetailsComponent', () => {
6
+ let component: BookDetailsComponent;
7
+ let fixture: ComponentFixture<BookDetailsComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ BookDetailsComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(BookDetailsComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/books/book-details/book-details.component.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { Component } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
 
3
 
4
  import { BookStoreService } from '../../shared/book-store.service';
5
  import { Book } from '../../shared/book';
@@ -10,16 +11,15 @@ import { Book } from '../../shared/book';
10
  styleUrls: ['./book-details.component.css']
11
  })
12
  export class BookDetailsComponent {
13
- book?: Book;
 
14
  constructor(
15
  private service: BookStoreService,
16
  private route: ActivatedRoute,
17
  private router: Router
18
  ) {
19
  const isbn = this.route.snapshot.paramMap.get('isbn')!;
20
- this.service.getSingle(isbn).subscribe(book => {
21
- this.book = book;
22
- });
23
  }
24
 
25
  removeBook(isbn: string) {
1
  import { Component } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
3
+ import { Observable } from 'rxjs';
4
 
5
  import { BookStoreService } from '../../shared/book-store.service';
6
  import { Book } from '../../shared/book';
11
  styleUrls: ['./book-details.component.css']
12
  })
13
  export class BookDetailsComponent {
14
+ book$: Observable<Book>;
15
+
16
  constructor(
17
  private service: BookStoreService,
18
  private route: ActivatedRoute,
19
  private router: Router
20
  ) {
21
  const isbn = this.route.snapshot.paramMap.get('isbn')!;
22
+ this.book$ = this.service.getSingle(isbn);
 
 
23
  }
24
 
25
  removeBook(isbn: string) {
src/app/books/book-list/book-list.component.html CHANGED
@@ -1,5 +1,5 @@
1
  <h1>Books</h1>
2
- <ul class="book-list">
3
  <li *ngFor="let book of books">
4
  <bm-book-list-item [book]="book"></bm-book-list-item>
5
  </li>
1
  <h1>Books</h1>
2
+ <ul class="book-list" *ngIf="books$ | async as books">
3
  <li *ngFor="let book of books">
4
  <bm-book-list-item [book]="book"></bm-book-list-item>
5
  </li>
src/app/books/book-list/book-list.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BookListComponent } from './book-list.component';
4
+
5
+ describe('BookListComponent', () => {
6
+ let component: BookListComponent;
7
+ let fixture: ComponentFixture<BookListComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ BookListComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(BookListComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/books/book-list/book-list.component.ts CHANGED
@@ -1,7 +1,8 @@
1
  import { Component } from '@angular/core';
 
2
 
3
- import { BookStoreService } from '../../shared/book-store.service';
4
  import { Book } from '../../shared/book';
 
5
 
6
  @Component({
7
  selector: 'bm-book-list',
@@ -9,11 +10,9 @@ import { Book } from '../../shared/book';
9
  styleUrls: ['./book-list.component.css']
10
  })
11
  export class BookListComponent {
12
- books: Book[] = [];
13
 
14
  constructor(private service: BookStoreService) {
15
- this.service.getAll().subscribe(books => {
16
- this.books = books;
17
- });
18
  }
19
  }
1
  import { Component } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
 
 
4
  import { Book } from '../../shared/book';
5
+ import { BookStoreService } from '../../shared/book-store.service';
6
 
7
  @Component({
8
  selector: 'bm-book-list',
10
  styleUrls: ['./book-list.component.css']
11
  })
12
  export class BookListComponent {
13
+ books$: Observable<Book[]>;
14
 
15
  constructor(private service: BookStoreService) {
16
+ this.books$ = this.service.getAll();
 
 
17
  }
18
  }
src/app/home/home.component.html CHANGED
@@ -3,3 +3,6 @@
3
  <a routerLink="/books" class="button red">
4
  Show book list
5
  </a>
 
 
 
3
  <a routerLink="/books" class="button red">
4
  Show book list
5
  </a>
6
+
7
+ <h2>Search</h2>
8
+ <bm-search></bm-search>
src/app/search/search.component.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <input type="search"
2
+ autocomplete="off"
3
+ aria-label="Search"
4
+ [class.loading]="isLoading"
5
+ #searchInput
6
+ (input)="input$.next(searchInput.value)">
7
+
8
+ <ul class="search-results" *ngIf="results$ | async as results">
9
+ <li *ngFor="let book of results">
10
+ <a [routerLink]="['/books', book.isbn]">
11
+ {{ book.title }}
12
+ <p role="doc-subtitle">{{ book.subtitle }}</p>
13
+ </a>
14
+ </li>
15
+ <li *ngIf="!results.length">No results</li>
16
+ </ul>
src/app/search/search.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { SearchComponent } from './search.component';
4
+
5
+ describe('SearchComponent', () => {
6
+ let component: SearchComponent;
7
+ let fixture: ComponentFixture<SearchComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ SearchComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(SearchComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/search/search.component.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { Subject, debounceTime, distinctUntilChanged, filter, switchMap, tap, Observable } from 'rxjs';
3
+
4
+ import { Book } from '../shared/book';
5
+ import { BookStoreService } from '../shared/book-store.service';
6
+
7
+ @Component({
8
+ selector: 'bm-search',
9
+ templateUrl: './search.component.html',
10
+ styleUrls: ['./search.component.css']
11
+ })
12
+ export class SearchComponent {
13
+ input$ = new Subject<string>();
14
+ isLoading = false;
15
+
16
+ results$: Observable<Book[]>;
17
+
18
+ constructor(private service: BookStoreService) {
19
+ this.results$ = this.input$.pipe(
20
+ filter(term => term.length >= 3),
21
+ debounceTime(500),
22
+ distinctUntilChanged(),
23
+ tap(() => this.isLoading = true),
24
+ switchMap(term => this.service.getAllSearch(term)),
25
+ tap(() => this.isLoading = false)
26
+ );
27
+ }
28
+ }
src/app/shared/book-store.service.http-testing-controller.spec.ts DELETED
@@ -1,66 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3
-
4
- import { Book } from './book';
5
- import { BookStoreService } from './book-store.service';
6
- import { HttpErrorResponse } from '@angular/common/http';
7
-
8
- describe('BookStoreService', () => {
9
- let service: BookStoreService;
10
- let httpMock: HttpTestingController;
11
- const expectedBooks = [
12
- { isbn: '111', title: 'Book 1', authors: [] },
13
- { isbn: '222', title: 'Book 2', authors: [] }
14
- ];
15
-
16
- beforeEach(() => {
17
- TestBed.configureTestingModule({
18
- imports: [HttpClientTestingModule]
19
- });
20
-
21
- service = TestBed.inject(BookStoreService);
22
- httpMock = TestBed.inject(HttpTestingController);
23
- });
24
-
25
- it('should GET a list of all books', () => {
26
-
27
- let receivedBooks!: Book[];
28
- service.getAll().subscribe(books => receivedBooks = books);
29
-
30
- // Request aus der Warteschlange holen
31
- const req = httpMock.expectOne(
32
- 'https://api5.angular-buch.com/books');
33
- expect(req.request.method).toEqual('GET');
34
-
35
- // flush -- jetzt werden die Bücher emittiert
36
- req.flush(expectedBooks);
37
-
38
- expect(receivedBooks).toHaveSize(2);
39
- expect(receivedBooks[0].isbn).toBe('111');
40
- expect(receivedBooks[1].isbn).toBe('222');
41
- });
42
-
43
- it('should throw a HttpErrorResponse on server errors', () => {
44
-
45
- let errorResponse!: HttpErrorResponse;
46
-
47
- service.getAll().subscribe({
48
- error: (error: any) => errorResponse = error
49
- });
50
-
51
- const req = httpMock.expectOne('https://api5.angular-buch.com/books');
52
-
53
- req.flush('Fehler!', {
54
- status: 500,
55
- statusText: 'Internal Server Error'
56
- });
57
-
58
- expect(errorResponse.status).toBe(500);
59
- expect(errorResponse.statusText).toBe('Internal Server Error');
60
- });
61
-
62
- afterEach(() => {
63
- // prüfen, ob kein Request übrig geblieben ist
64
- httpMock.verify();
65
- });
66
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/shared/book-store.service.mock.spec.ts DELETED
@@ -1,48 +0,0 @@
1
- import { HttpClient } from '@angular/common/http';
2
- import { TestBed } from '@angular/core/testing';
3
- import { Observable, of } from 'rxjs';
4
-
5
- import { Book } from '../shared/book';
6
- import { BookStoreService } from './book-store.service';
7
-
8
- describe('BookStoreService', () => {
9
- let service: BookStoreService;
10
- let httpMock: {
11
- get: (url: string) => Observable<Book[]>;
12
- };
13
-
14
- beforeEach(() => {
15
-
16
- const expectedBooks: Book[] = [
17
- { isbn: '111', title: 'Book 1', authors: [] },
18
- { isbn: '222', title: 'Book 2', authors: [] }
19
- ];
20
-
21
- httpMock = {
22
- get: () => of(expectedBooks)
23
- };
24
-
25
- spyOn(httpMock, 'get').and.callThrough();
26
-
27
- TestBed.configureTestingModule({
28
- providers: [
29
- { provide: HttpClient, useValue: httpMock },
30
- BookStoreService
31
- ]
32
- });
33
- service = TestBed.inject(BookStoreService);
34
- });
35
-
36
- it('should GET a list of all books', () => {
37
-
38
- let receivedBooks!: Book[];
39
- service.getAll().subscribe(b => receivedBooks = b);
40
-
41
- expect(receivedBooks).toHaveSize(2);
42
- expect(receivedBooks[0].isbn).toBe('111');
43
- expect(receivedBooks[1].isbn).toBe('222');
44
-
45
- // NEU
46
- expect(httpMock.get).toHaveBeenCalledOnceWith('https://api5.angular-buch.com/books');
47
- });
48
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/shared/book-store.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { BookStoreService } from './book-store.service';
4
+
5
+ describe('BookStoreService', () => {
6
+ let service: BookStoreService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(BookStoreService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/shared/book-store.service.stub.spec.ts DELETED
@@ -1,39 +0,0 @@
1
- import { HttpClient } from '@angular/common/http';
2
- import { TestBed } from '@angular/core/testing';
3
- import { of } from 'rxjs';
4
-
5
- import { Book } from './book';
6
- import { BookStoreService } from './book-store.service';
7
-
8
- describe('BookStoreService', () => {
9
- let service: BookStoreService;
10
-
11
- beforeEach(() => {
12
-
13
- const expectedBooks: Book[] = [
14
- { isbn: '111', title: 'Book 1', authors: [] },
15
- { isbn: '222', title: 'Book 2', authors: [] }
16
- ];
17
-
18
- const httpStub = {
19
- get: () => of(expectedBooks)
20
- };
21
-
22
- TestBed.configureTestingModule({
23
- providers: [
24
- { provide: HttpClient, useValue: httpStub }
25
- ]
26
- });
27
- service = TestBed.inject(BookStoreService);
28
- });
29
-
30
- it('should GET a list of all books', () => {
31
-
32
- let receivedBooks!: Book[];
33
- service.getAll().subscribe(b => receivedBooks = b);
34
-
35
- expect(receivedBooks).toHaveSize(2);
36
- expect(receivedBooks[0].isbn).toBe('111');
37
- expect(receivedBooks[1].isbn).toBe('222');
38
- });
39
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/shared/book-store.service.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { HttpClient } from '@angular/common/http';
2
  import { Injectable } from '@angular/core';
3
- import { Observable } from 'rxjs';
4
 
5
  import { Book } from './book';
6
 
@@ -13,7 +13,12 @@ export class BookStoreService {
13
  constructor(private http: HttpClient) {}
14
 
15
  getAll(): Observable<Book[]> {
16
- return this.http.get<Book[]>(`${this.apiUrl}/books`);
 
 
 
 
 
17
  }
18
 
19
  getSingle(isbn: string): Observable<Book> {
@@ -23,4 +28,13 @@ export class BookStoreService {
23
  remove(isbn: string): Observable<unknown> {
24
  return this.http.delete(`${this.apiUrl}/books/${isbn}`);
25
  }
 
 
 
 
 
 
 
 
 
26
  }
1
  import { HttpClient } from '@angular/common/http';
2
  import { Injectable } from '@angular/core';
3
+ import { Observable, catchError, of } from 'rxjs';
4
 
5
  import { Book } from './book';
6
 
13
  constructor(private http: HttpClient) {}
14
 
15
  getAll(): Observable<Book[]> {
16
+ return this.http.get<Book[]>(`${this.apiUrl}/books`).pipe(
17
+ catchError(err => {
18
+ console.error(err);
19
+ return of([]);
20
+ })
21
+ );
22
  }
23
 
24
  getSingle(isbn: string): Observable<Book> {
28
  remove(isbn: string): Observable<unknown> {
29
  return this.http.delete(`${this.apiUrl}/books/${isbn}`);
30
  }
31
+
32
+ getAllSearch(term: string): Observable<Book[]> {
33
+ return this.http.get<Book[]>(`${this.apiUrl}/books/search/${term}`).pipe(
34
+ catchError(err => {
35
+ console.error(err);
36
+ return of([]);
37
+ })
38
+ );
39
+ }
40
  }