@@ -114,7 +114,8 @@
|
|
114 |
},
|
115 |
"cli": {
|
116 |
"schematicCollections": [
|
117 |
-
"angular-eslint"
|
|
|
118 |
]
|
119 |
}
|
120 |
-
}
|
114 |
},
|
115 |
"cli": {
|
116 |
"schematicCollections": [
|
117 |
+
"angular-eslint",
|
118 |
+
"@ngrx/schematics"
|
119 |
]
|
120 |
}
|
121 |
+
}
|
@@ -20,6 +20,9 @@
|
|
20 |
"@angular/platform-browser": "^19.1.0",
|
21 |
"@angular/platform-browser-dynamic": "^19.1.0",
|
22 |
"@angular/router": "^19.1.0",
|
|
|
|
|
|
|
23 |
"angular-date-value-accessor": "^3.0.0",
|
24 |
"book-monkey5-styles": "^1.0.4",
|
25 |
"rxjs": "~7.8.0",
|
@@ -30,10 +33,12 @@
|
|
30 |
"@angular-devkit/build-angular": "^19.1.1",
|
31 |
"@angular/cli": "^19.1.1",
|
32 |
"@angular/compiler-cli": "^19.1.0",
|
|
|
33 |
"@types/jasmine": "~5.1.0",
|
34 |
"angular-eslint": "19.0.2",
|
35 |
"eslint": "^9.16.0",
|
36 |
"jasmine-core": "~5.5.0",
|
|
|
37 |
"karma": "~6.4.0",
|
38 |
"karma-chrome-launcher": "~3.2.0",
|
39 |
"karma-coverage": "~2.2.0",
|
20 |
"@angular/platform-browser": "^19.1.0",
|
21 |
"@angular/platform-browser-dynamic": "^19.1.0",
|
22 |
"@angular/router": "^19.1.0",
|
23 |
+
"@ngrx/effects": "^19.0.0",
|
24 |
+
"@ngrx/store": "^19.0.0",
|
25 |
+
"@ngrx/store-devtools": "^19.0.0",
|
26 |
"angular-date-value-accessor": "^3.0.0",
|
27 |
"book-monkey5-styles": "^1.0.4",
|
28 |
"rxjs": "~7.8.0",
|
33 |
"@angular-devkit/build-angular": "^19.1.1",
|
34 |
"@angular/cli": "^19.1.1",
|
35 |
"@angular/compiler-cli": "^19.1.0",
|
36 |
+
"@ngrx/schematics": "^19.0.0",
|
37 |
"@types/jasmine": "~5.1.0",
|
38 |
"angular-eslint": "19.0.2",
|
39 |
"eslint": "^9.16.0",
|
40 |
"jasmine-core": "~5.5.0",
|
41 |
+
"jasmine-marbles": "^0.9.2",
|
42 |
"karma": "~6.4.0",
|
43 |
"karma-chrome-launcher": "~3.2.0",
|
44 |
"karma-coverage": "~2.2.0",
|
@@ -1,6 +1,9 @@
|
|
1 |
-
import { NgModule } from '@angular/core';
|
2 |
import { provideHttpClient, withInterceptorsFromDi, 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';
|
@@ -17,6 +20,9 @@ import { AuthInterceptor } from './shared/auth.interceptor';
|
|
17 |
imports: [
|
18 |
BrowserModule,
|
19 |
AppRoutingModule,
|
|
|
|
|
|
|
20 |
],
|
21 |
providers: [
|
22 |
provideHttpClient(withInterceptorsFromDi()),
|
1 |
+
import { NgModule, isDevMode } from '@angular/core';
|
2 |
import { provideHttpClient, withInterceptorsFromDi, 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';
|
20 |
imports: [
|
21 |
BrowserModule,
|
22 |
AppRoutingModule,
|
23 |
+
StoreModule.forRoot({}, {}),
|
24 |
+
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }),
|
25 |
+
EffectsModule.forRoot([]),
|
26 |
],
|
27 |
providers: [
|
28 |
provideHttpClient(withInterceptorsFromDi()),
|
@@ -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>
|
@@ -1,8 +1,10 @@
|
|
1 |
import { Component } from '@angular/core';
|
|
|
2 |
import { Observable } from 'rxjs';
|
3 |
|
4 |
import { Book } from '../../shared/book';
|
5 |
-
import {
|
|
|
6 |
|
7 |
@Component({
|
8 |
selector: 'bm-book-list',
|
@@ -12,8 +14,12 @@ import { BookStoreService } from '../../shared/book-store.service';
|
|
12 |
})
|
13 |
export class BookListComponent {
|
14 |
books$: Observable<Book[]>;
|
|
|
15 |
|
16 |
-
constructor(private
|
17 |
-
this.books$ = this.
|
|
|
|
|
|
|
18 |
}
|
19 |
}
|
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',
|
14 |
})
|
15 |
export class BookListComponent {
|
16 |
books$: Observable<Book[]>;
|
17 |
+
loading$: Observable<boolean>;
|
18 |
|
19 |
+
constructor(private store: Store) {
|
20 |
+
this.books$ = this.store.select(selectAllBooks);
|
21 |
+
this.loading$ = this.store.select(selectBooksLoading);
|
22 |
+
|
23 |
+
this.store.dispatch(loadBooks());
|
24 |
}
|
25 |
}
|
@@ -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 { }
|
@@ -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 |
+
);
|
@@ -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 |
+
}
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { inject, 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 |
+
private actions$ = inject(Actions);
|
13 |
+
private service = inject(BookStoreService);
|
14 |
+
|
15 |
+
loadBooks$ = createEffect(() => {
|
16 |
+
return this.actions$.pipe(
|
17 |
+
ofType(BookActions.loadBooks),
|
18 |
+
switchMap(() =>
|
19 |
+
this.service.getAll().pipe(
|
20 |
+
map(data => BookActions.loadBooksSuccess({ data })),
|
21 |
+
catchError(error => of(BookActions.loadBooksFailure({ error: error.message })))
|
22 |
+
)
|
23 |
+
)
|
24 |
+
);
|
25 |
+
});
|
26 |
+
}
|
@@ -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 |
+
);
|
@@ -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 |
+
);
|
@@ -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 |
+
}
|