|
@@ -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 |
+
}
|
|
@@ -19,6 +19,9 @@
|
|
| 19 |
"@angular/platform-browser": "^19.2.0",
|
| 20 |
"@angular/platform-browser-dynamic": "^19.2.0",
|
| 21 |
"@angular/router": "^19.2.0",
|
|
|
|
|
|
|
|
|
|
| 22 |
"angular-date-value-accessor": "^3.0.0",
|
| 23 |
"book-monkey5-styles": "^1.0.4",
|
| 24 |
"rxjs": "~7.8.0",
|
|
@@ -29,10 +32,12 @@
|
|
| 29 |
"@angular-devkit/build-angular": "^19.2.1",
|
| 30 |
"@angular/cli": "^19.2.1",
|
| 31 |
"@angular/compiler-cli": "^19.2.0",
|
|
|
|
| 32 |
"@types/jasmine": "~5.1.0",
|
| 33 |
"angular-eslint": "19.2.0",
|
| 34 |
"eslint": "^9.21.0",
|
| 35 |
"jasmine-core": "~5.6.0",
|
|
|
|
| 36 |
"karma": "~6.4.0",
|
| 37 |
"karma-chrome-launcher": "~3.2.0",
|
| 38 |
"karma-coverage": "~2.2.0",
|
| 19 |
"@angular/platform-browser": "^19.2.0",
|
| 20 |
"@angular/platform-browser-dynamic": "^19.2.0",
|
| 21 |
"@angular/router": "^19.2.0",
|
| 22 |
+
"@ngrx/effects": "^19.0.1",
|
| 23 |
+
"@ngrx/store": "^19.0.1",
|
| 24 |
+
"@ngrx/store-devtools": "^19.0.1",
|
| 25 |
"angular-date-value-accessor": "^3.0.0",
|
| 26 |
"book-monkey5-styles": "^1.0.4",
|
| 27 |
"rxjs": "~7.8.0",
|
| 32 |
"@angular-devkit/build-angular": "^19.2.1",
|
| 33 |
"@angular/cli": "^19.2.1",
|
| 34 |
"@angular/compiler-cli": "^19.2.0",
|
| 35 |
+
"@ngrx/schematics": "^19.0.1",
|
| 36 |
"@types/jasmine": "~5.1.0",
|
| 37 |
"angular-eslint": "19.2.0",
|
| 38 |
"eslint": "^9.21.0",
|
| 39 |
"jasmine-core": "~5.6.0",
|
| 40 |
+
"jasmine-marbles": "^0.9.2",
|
| 41 |
"karma": "~6.4.0",
|
| 42 |
"karma-chrome-launcher": "~3.2.0",
|
| 43 |
"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 |
+
}
|