Differenzansicht 16e-ngrx
im Vergleich zu 16-guards

← Zurück zur Ãœbersicht | Demo | Quelltext auf GitHub
.eslintrc.json CHANGED
@@ -42,6 +42,14 @@
42
  "plugin:@angular-eslint/template/accessibility"
43
  ],
44
  "rules": {}
 
 
 
 
 
 
 
 
45
  }
46
  ]
47
- }
42
  "plugin:@angular-eslint/template/accessibility"
43
  ],
44
  "rules": {}
45
+ },
46
+ {
47
+ "files": [
48
+ "*.ts"
49
+ ],
50
+ "extends": [
51
+ "plugin:@ngrx/recommended"
52
+ ]
53
  }
54
  ]
55
+ }
angular.json CHANGED
@@ -113,7 +113,8 @@
113
  },
114
  "cli": {
115
  "schematicCollections": [
116
- "@angular-eslint/schematics"
 
117
  ]
118
  }
119
- }
113
  },
114
  "cli": {
115
  "schematicCollections": [
116
+ "@angular-eslint/schematics",
117
+ "@ngrx/schematics"
118
  ]
119
  }
120
+ }
package.json CHANGED
@@ -19,6 +19,9 @@
19
  "@angular/platform-browser": "^17.3.0",
20
  "@angular/platform-browser-dynamic": "^17.3.0",
21
  "@angular/router": "^17.3.0",
 
 
 
22
  "angular-date-value-accessor": "^3.0.0",
23
  "book-monkey5-styles": "^1.0.4",
24
  "rxjs": "~7.8.0",
@@ -34,11 +37,14 @@
34
  "@angular-eslint/template-parser": "17.3.0",
35
  "@angular/cli": "^17.3.6",
36
  "@angular/compiler-cli": "^17.3.0",
 
 
37
  "@types/jasmine": "~5.1.0",
38
  "@typescript-eslint/eslint-plugin": "7.2.0",
39
  "@typescript-eslint/parser": "7.2.0",
40
  "eslint": "^8.57.0",
41
  "jasmine-core": "~5.1.0",
 
42
  "karma": "~6.4.0",
43
  "karma-chrome-launcher": "~3.2.0",
44
  "karma-coverage": "~2.2.0",
19
  "@angular/platform-browser": "^17.3.0",
20
  "@angular/platform-browser-dynamic": "^17.3.0",
21
  "@angular/router": "^17.3.0",
22
+ "@ngrx/effects": "^17.2.0",
23
+ "@ngrx/store": "^17.2.0",
24
+ "@ngrx/store-devtools": "^17.2.0",
25
  "angular-date-value-accessor": "^3.0.0",
26
  "book-monkey5-styles": "^1.0.4",
27
  "rxjs": "~7.8.0",
37
  "@angular-eslint/template-parser": "17.3.0",
38
  "@angular/cli": "^17.3.6",
39
  "@angular/compiler-cli": "^17.3.0",
40
+ "@ngrx/eslint-plugin": "^17.2.0",
41
+ "@ngrx/schematics": "^17.2.0",
42
  "@types/jasmine": "~5.1.0",
43
  "@typescript-eslint/eslint-plugin": "7.2.0",
44
  "@typescript-eslint/parser": "7.2.0",
45
  "eslint": "^8.57.0",
46
  "jasmine-core": "~5.1.0",
47
+ "jasmine-marbles": "^0.9.2",
48
  "karma": "~6.4.0",
49
  "karma-chrome-launcher": "~3.2.0",
50
  "karma-coverage": "~2.2.0",
src/app/admin/book-create/book-create.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BookCreateComponent } from './book-create.component';
4
+
5
+ describe('BookCreateComponent', () => {
6
+ let component: BookCreateComponent;
7
+ let fixture: ComponentFixture<BookCreateComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ BookCreateComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(BookCreateComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/admin/book-edit/book-edit.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BookEditComponent } from './book-edit.component';
4
+
5
+ describe('BookEditComponent', () => {
6
+ let component: BookEditComponent;
7
+ let fixture: ComponentFixture<BookEditComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ BookEditComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(BookEditComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/admin/book-form/book-form.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BookFormComponent } from './book-form.component';
4
+
5
+ describe('BookFormComponent', () => {
6
+ let component: BookFormComponent;
7
+ let fixture: ComponentFixture<BookFormComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ BookFormComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(BookFormComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/admin/form-errors/form-errors.component.spec.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { FormErrorsComponent } from './form-errors.component';
4
+
5
+ describe('FormErrorsComponent', () => {
6
+ let component: FormErrorsComponent;
7
+ let fixture: ComponentFixture<FormErrorsComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ FormErrorsComponent ]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(FormErrorsComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
src/app/admin/shared/async-validators.service.spec.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { AsyncValidatorsService } from './async-validators.service';
4
+
5
+ describe('AsyncValidatorsService', () => {
6
+ let service: AsyncValidatorsService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({});
10
+ service = TestBed.inject(AsyncValidatorsService);
11
+ });
12
+
13
+ it('should be created', () => {
14
+ expect(service).toBeTruthy();
15
+ });
16
+ });
src/app/app.module.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { NgModule } from '@angular/core';
2
  import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
3
  import { BrowserModule } from '@angular/platform-browser';
 
 
 
4
 
5
  import { AppRoutingModule } from './app-routing.module';
6
  import { AppComponent } from './app.component';
@@ -18,6 +21,9 @@ import { AuthInterceptor } from './shared/auth.interceptor';
18
  BrowserModule,
19
  AppRoutingModule,
20
  HttpClientModule,
 
 
 
21
  ],
22
  providers: [
23
  {
1
+ import { NgModule, isDevMode } from '@angular/core';
2
  import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
3
  import { BrowserModule } from '@angular/platform-browser';
4
+ import { StoreModule } from '@ngrx/store';
5
+ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
6
+ import { EffectsModule } from '@ngrx/effects';
7
 
8
  import { AppRoutingModule } from './app-routing.module';
9
  import { AppComponent } from './app.component';
21
  BrowserModule,
22
  AppRoutingModule,
23
  HttpClientModule,
24
+ StoreModule.forRoot({}, {}),
25
+ StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }),
26
+ EffectsModule.forRoot([]),
27
  ],
28
  providers: [
29
  {
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-list/book-list.component.html CHANGED
@@ -7,3 +7,5 @@
7
  No books available.
8
  </li>
9
  </ul>
 
 
7
  No books available.
8
  </li>
9
  </ul>
10
+
11
+ <div class="loader" *ngIf="loading$ | async">Loading ...</div>
src/app/books/book-list/book-list.component.spec.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { provideMockStore } from '@ngrx/store/testing';
3
+ import { selectAllBooks, selectBooksLoading } from '../store/book.selectors';
4
+
5
+ import { BookListComponent } from './book-list.component';
6
+
7
+ describe('BookListComponent', () => {
8
+ beforeEach(async () => {
9
+ await TestBed.configureTestingModule({
10
+ declarations: [ BookListComponent ],
11
+ providers: [
12
+ provideMockStore({
13
+ selectors: [
14
+ { selector: selectBooksLoading, value: true },
15
+ { selector: selectAllBooks, value: [] },
16
+ ]
17
+ })
18
+ ]
19
+ })
20
+ .compileComponents();
21
+ });
22
+
23
+ it('should show a loading text', () => {
24
+ const fixture = TestBed.createComponent(BookListComponent);
25
+ fixture.detectChanges();
26
+ const element = fixture.nativeElement;
27
+ expect(element.querySelector('.loader').textContent).toBe('Loading ...');
28
+ });
29
+ });
src/app/books/book-list/book-list.component.ts CHANGED
@@ -1,8 +1,10 @@
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',
@@ -11,8 +13,12 @@ import { BookStoreService } from '../../shared/book-store.service';
11
  })
12
  export class BookListComponent {
13
  books$: Observable<Book[]>;
 
14
 
15
- constructor(private service: BookStoreService) {
16
- this.books$ = this.service.getAll();
 
 
 
17
  }
18
  }
1
  import { Component } from '@angular/core';
2
+ import { Store } from '@ngrx/store';
3
  import { Observable } from 'rxjs';
4
 
5
  import { Book } from '../../shared/book';
6
+ import { loadBooks } from '../store/book.actions';
7
+ import { selectAllBooks, selectBooksLoading } from '../store/book.selectors';
8
 
9
  @Component({
10
  selector: 'bm-book-list',
13
  })
14
  export class BookListComponent {
15
  books$: Observable<Book[]>;
16
+ loading$: Observable<boolean>;
17
 
18
+ constructor(private store: Store) {
19
+ this.books$ = this.store.select(selectAllBooks);
20
+ this.loading$ = this.store.select(selectBooksLoading);
21
+
22
+ this.store.dispatch(loadBooks());
23
  }
24
  }
src/app/books/books.module.ts CHANGED
@@ -1,5 +1,7 @@
1
  import { NgModule } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
 
 
3
 
4
  import { BooksRoutingModule } from './books-routing.module';
5
  import { BookListComponent } from './book-list/book-list.component';
@@ -8,6 +10,8 @@ import { BookDetailsComponent } from './book-details/book-details.component';
8
  import { IsbnPipe } from './shared/isbn.pipe';
9
  import { ConfirmDirective } from './shared/confirm.directive';
10
  import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
 
 
11
 
12
  @NgModule({
13
  declarations: [
@@ -20,7 +24,9 @@ import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
20
  ],
21
  imports: [
22
  CommonModule,
23
- BooksRoutingModule
 
 
24
  ]
25
  })
26
  export class BooksModule { }
1
  import { NgModule } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
+ import { StoreModule } from '@ngrx/store';
4
+ import { EffectsModule } from '@ngrx/effects';
5
 
6
  import { BooksRoutingModule } from './books-routing.module';
7
  import { BookListComponent } from './book-list/book-list.component';
10
  import { IsbnPipe } from './shared/isbn.pipe';
11
  import { ConfirmDirective } from './shared/confirm.directive';
12
  import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
13
+ import { BookEffects } from './store/book.effects';
14
+ import * as fromBook from './store/book.reducer';
15
 
16
  @NgModule({
17
  declarations: [
24
  ],
25
  imports: [
26
  CommonModule,
27
+ BooksRoutingModule,
28
+ StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer),
29
+ EffectsModule.forFeature([BookEffects])
30
  ]
31
  })
32
  export class BooksModule { }
src/app/books/shared/loggedin-only.directive.spec.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ import { LoggedinOnlyDirective } from './loggedin-only.directive';
2
+
3
+ describe('LoggedinOnlyDirective', () => {
4
+ it('should create an instance', () => {
5
+ const directive = new LoggedinOnlyDirective();
6
+ expect(directive).toBeTruthy();
7
+ });
8
+ });
src/app/books/store/book.actions.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createAction, props } from '@ngrx/store';
2
+
3
+ import { Book } from '../../shared/book';
4
+
5
+ export const loadBooks = createAction(
6
+ '[Book] Load Books'
7
+ );
8
+
9
+ export const loadBooksSuccess = createAction(
10
+ '[Book] Load Books Success',
11
+ props<{ data: Book[] }>()
12
+ );
13
+
14
+ export const loadBooksFailure = createAction(
15
+ '[Book] Load Books Failure',
16
+ props<{ error: string }>()
17
+ );
src/app/books/store/book.effects.alternative.spec.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { Actions } from '@ngrx/effects';
3
+ import { provideMockActions } from '@ngrx/effects/testing';
4
+ import { provideMockStore, MockStore } from '@ngrx/store/testing';
5
+ import { cold, hot } from 'jasmine-marbles';
6
+ import { of } from 'rxjs';
7
+ import { BookStoreService } from '../../shared/book-store.service';
8
+
9
+ import { loadBooks, loadBooksSuccess } from './book.actions';
10
+ import { BookEffects } from './book.effects.alternative';
11
+ import { initialState } from './book.reducer';
12
+ import { book } from './test-helper';
13
+
14
+ describe('BookEffects', () => {
15
+ let actions$: Actions;
16
+ let effects: BookEffects;
17
+
18
+ beforeEach(() => {
19
+ TestBed.configureTestingModule({
20
+ providers: [
21
+ BookEffects,
22
+ provideMockActions(() => actions$),
23
+ provideMockStore({
24
+ initialState: { book: initialState }
25
+ }),
26
+ {
27
+ provide: BookStoreService,
28
+ useValue: {
29
+ getAll: () => of([])
30
+ }
31
+ }
32
+ ]
33
+ });
34
+
35
+ effects = TestBed.inject(BookEffects);
36
+ });
37
+
38
+ it('should fire loadBooksSuccess for loadBooks if store is empty', () => {
39
+
40
+ actions$ = hot('--a', { a: loadBooks() });
41
+ const expected = cold('--b', { b: loadBooksSuccess({ data: [] }) });
42
+
43
+ expect(effects.loadBooks$).toBeObservable(expected);
44
+ });
45
+
46
+ it('should do nothing if store is already filled', () => {
47
+
48
+ const store = TestBed.inject(MockStore);
49
+ store.setState({
50
+ book: {
51
+ books: [book(1)],
52
+ loading: false
53
+ }
54
+ });
55
+
56
+ actions$ = hot('--a', { a: loadBooks() });
57
+ const expected = cold('---');
58
+
59
+ expect(effects.loadBooks$).toBeObservable(expected);
60
+ });
61
+ });
src/app/books/store/book.effects.alternative.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { Store } from '@ngrx/store';
3
+ import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
4
+ import { catchError, filter, map, switchMap } from 'rxjs/operators';
5
+ import { of } from 'rxjs';
6
+
7
+ import * as BookActions from './book.actions';
8
+ import { BookStoreService } from '../../shared/book-store.service';
9
+ import { selectAllBooks } from './book.selectors';
10
+
11
+ @Injectable()
12
+ export class BookEffects {
13
+
14
+ loadBooks$ = createEffect(() => {
15
+ return this.actions$.pipe(
16
+ ofType(BookActions.loadBooks),
17
+ concatLatestFrom(() => this.store.select(selectAllBooks)),
18
+ filter(([action, books]) => !books.length),
19
+ switchMap(() =>
20
+ this.service.getAll().pipe(
21
+ map(data => BookActions.loadBooksSuccess({ data })),
22
+ catchError(error => of(BookActions.loadBooksFailure({ error: error.message })))
23
+ )
24
+ )
25
+ );
26
+ });
27
+
28
+ constructor(
29
+ private actions$: Actions,
30
+ private service: BookStoreService,
31
+ private store: Store
32
+ ) {}
33
+ }
src/app/books/store/book.effects.marble.spec.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { provideMockActions } from '@ngrx/effects/testing';
3
+ import { Action } from '@ngrx/store';
4
+ import { Observable, of } from 'rxjs';
5
+ import { BookStoreService } from '../../shared/book-store.service';
6
+ import { loadBooks, loadBooksSuccess } from './book.actions';
7
+ import { hot, cold } from 'jasmine-marbles';
8
+
9
+ import { BookEffects } from './book.effects';
10
+ import { book } from './test-helper';
11
+
12
+ describe('BookEffects', () => {
13
+ let actions$: Observable<any>;
14
+ let effects: BookEffects;
15
+
16
+ beforeEach(() => {
17
+ TestBed.configureTestingModule({
18
+ providers: [
19
+ BookEffects,
20
+ provideMockActions(() => actions$),
21
+ {
22
+ provide: BookStoreService,
23
+ useValue: { getAll: () => of([]) }
24
+ }
25
+ ]
26
+ });
27
+
28
+ effects = TestBed.inject(BookEffects);
29
+ });
30
+
31
+ it('should fire loadBooksSuccess for loadBooks', () => {
32
+ const books = [book(1), book(2), book(3)];
33
+
34
+ // Implementierung von getAll() ersetzen
35
+ const bs = TestBed.inject(BookStoreService);
36
+ spyOn(bs, 'getAll').and.callFake(() => of(books));
37
+
38
+ // Action auslösen
39
+ actions$ = hot('--a', { a: loadBooks() });
40
+ const expected = cold('--b', { b: loadBooksSuccess({ data: books }) });
41
+
42
+ // Datenströme vergleichen
43
+ expect(effects.loadBooks$).toBeObservable(expected);
44
+
45
+ // Serviceaufruf prüfen
46
+ expect(bs.getAll).toHaveBeenCalled();
47
+ });
48
+ });
src/app/books/store/book.effects.spec.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { provideMockActions } from '@ngrx/effects/testing';
3
+ import { Action } from '@ngrx/store';
4
+ import { Observable, of } from 'rxjs';
5
+ import { BookStoreService } from '../../shared/book-store.service';
6
+ import { loadBooks, loadBooksSuccess } from './book.actions';
7
+
8
+ import { BookEffects } from './book.effects';
9
+ import { book } from './test-helper';
10
+
11
+ describe('BookEffects', () => {
12
+ let actions$: Observable<any>;
13
+ let effects: BookEffects;
14
+
15
+ beforeEach(() => {
16
+ TestBed.configureTestingModule({
17
+ providers: [
18
+ BookEffects,
19
+ provideMockActions(() => actions$),
20
+ {
21
+ provide: BookStoreService,
22
+ useValue: { getAll: () => of([]) }
23
+ }
24
+ ]
25
+ });
26
+
27
+ effects = TestBed.inject(BookEffects);
28
+ });
29
+
30
+ it('should fire loadBooksSuccess for loadBooks', () => {
31
+ const books = [book(1), book(2), book(3)];
32
+
33
+ // Implementierung von getAll() ersetzen
34
+ const bs = TestBed.inject(BookStoreService);
35
+ spyOn(bs, 'getAll').and.callFake(() => of(books));
36
+
37
+ // Action auslösen
38
+ actions$ = of(loadBooks());
39
+
40
+ // Actions aus Effect empfangen
41
+ let dispatchedAction: Action | undefined;
42
+ effects.loadBooks$.subscribe(action => {
43
+ dispatchedAction = action;
44
+ });
45
+
46
+ // Actions vergleichen
47
+ const expectedAction = loadBooksSuccess({ data: books });
48
+ expect(dispatchedAction).toEqual(expectedAction);
49
+
50
+ // Serviceaufruf prüfen
51
+ expect(bs.getAll).toHaveBeenCalled();
52
+ });
53
+ });
src/app/books/store/book.effects.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { Actions, createEffect, ofType } from '@ngrx/effects';
3
+ import { catchError, map, switchMap } from 'rxjs/operators';
4
+ import { of } from 'rxjs';
5
+
6
+ import * as BookActions from './book.actions';
7
+ import { BookStoreService } from '../../shared/book-store.service';
8
+
9
+ @Injectable()
10
+ export class BookEffects {
11
+
12
+ loadBooks$ = createEffect(() => {
13
+ return this.actions$.pipe(
14
+ ofType(BookActions.loadBooks),
15
+ switchMap(() =>
16
+ this.service.getAll().pipe(
17
+ map(data => BookActions.loadBooksSuccess({ data })),
18
+ catchError(error => of(BookActions.loadBooksFailure({ error: error.message })))
19
+ )
20
+ )
21
+ );
22
+ });
23
+
24
+ constructor(
25
+ private actions$: Actions,
26
+ private service: BookStoreService
27
+ ) {}
28
+ }
src/app/books/store/book.reducer.spec.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadBooks } from './book.actions';
2
+ import { reducer } from './book.reducer';
3
+
4
+ describe('Book Reducer', () => {
5
+ it('should enable the loading flag for loadBooks', () => {
6
+ const state = {
7
+ books: [],
8
+ loading: false
9
+ };
10
+ const action = loadBooks();
11
+
12
+ const newState = reducer(state, action);
13
+ expect(newState.loading).toBe(true);
14
+ });
15
+ });
src/app/books/store/book.reducer.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createReducer, on } from '@ngrx/store';
2
+
3
+ import { Book } from '../../shared/book';
4
+ import * as BookActions from './book.actions';
5
+
6
+ export const bookFeatureKey = 'book';
7
+
8
+ export interface State {
9
+ books: Book[];
10
+ loading: boolean;
11
+ }
12
+
13
+ export const initialState: State = {
14
+ books: [],
15
+ loading: false
16
+ };
17
+
18
+ export const reducer = createReducer(
19
+ initialState,
20
+
21
+ on(BookActions.loadBooks, (state): State => {
22
+ return { ...state, loading: true };
23
+ }),
24
+
25
+ on(BookActions.loadBooksSuccess, (state, action): State => {
26
+ return {
27
+ ...state,
28
+ books: action.data,
29
+ loading: false
30
+ };
31
+ }),
32
+
33
+ on(BookActions.loadBooksFailure, (state, action): State => {
34
+ return { ...state, loading: false };
35
+ }),
36
+ );
src/app/books/store/book.selectors.1.spec.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { selectAllBooks } from './book.selectors';
2
+ import { book } from './test-helper';
3
+
4
+ describe('Book Selectors', () => {
5
+ it('should select all books', () => {
6
+ const books = [book(1), book(2), book(3)];
7
+ const bookState = { books, loading: false };
8
+
9
+ const result = selectAllBooks.projector(bookState);
10
+ expect(result).toEqual(books);
11
+ });
12
+ });
src/app/books/store/book.selectors.spec.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { selectAllBooks } from './book.selectors';
2
+ import { book } from './test-helper';
3
+
4
+ describe('Book Selectors', () => {
5
+ it('should select all books', () => {
6
+ const books = [book(1), book(2), book(3)];
7
+ const state = {
8
+ book: { books, loading: false }
9
+ };
10
+
11
+ const result = selectAllBooks(state);
12
+ expect(result).toEqual(books);
13
+ });
14
+ });
src/app/books/store/book.selectors.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createFeatureSelector, createSelector } from '@ngrx/store';
2
+ import * as fromBook from './book.reducer';
3
+
4
+ export const selectBookState = createFeatureSelector<fromBook.State>(
5
+ fromBook.bookFeatureKey
6
+ );
7
+
8
+ export const selectBooksLoading = createSelector(
9
+ selectBookState,
10
+ state => state.loading
11
+ );
12
+
13
+ export const selectAllBooks = createSelector(
14
+ selectBookState,
15
+ state => state.books
16
+ );
src/app/books/store/test-helper.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ import { Book } from '../../shared/book';
2
+
3
+ export function book(i: number): Book {
4
+ return {
5
+ isbn: 'isbn' + i,
6
+ title: 'title' + i,
7
+ authors: []
8
+ };
9
+ }
src/app/home/home.component.spec.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { NO_ERRORS_SCHEMA } from '@angular/core';
2
  import { ComponentFixture, TestBed } from '@angular/core/testing';
3
 
4
  import { HomeComponent } from './home.component';
@@ -9,8 +8,7 @@ describe('HomeComponent', () => {
9
 
10
  beforeEach(async () => {
11
  await TestBed.configureTestingModule({
12
- declarations: [ HomeComponent ],
13
- schemas: [NO_ERRORS_SCHEMA]
14
  })
15
  .compileComponents();
16
 
 
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
 
3
  import { HomeComponent } from './home.component';
8
 
9
  beforeEach(async () => {
10
  await TestBed.configureTestingModule({
11
+ declarations: [ HomeComponent ]
 
12
  })
13
  .compileComponents();
14
 
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/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
+ });