Skip to content

Commit 997b8a1

Browse files
author
Lukas Ruebbelke
authored
Merge pull request onehungrymind#1 from onehungrymind/feature/ngrx
First cut at NGRX
2 parents 65ba8d5 + a510bfe commit 997b8a1

File tree

12 files changed

+352
-31
lines changed

12 files changed

+352
-31
lines changed

.angulardoc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"repoId": "e66f58f4-e577-4eb4-b671-d7505ea7ad13",
3+
"lastSync": 0
4+
}

apps/dashboard/src/app/app.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { WidgetDetailComponent } from './widgets/widget-detail/widget-detail.com
1818
import { WidgetsListComponent } from './widgets/widgets-list/widgets-list.component';
1919
import { ItemsTotalComponent } from './items/items-total/items-total.component';
2020
import { WidgetsTotalComponent } from './widgets/widgets-total/widgets-total.component';
21+
import { StoreModule } from '@ngrx/store';
22+
import { EffectsModule } from '@ngrx/effects';
23+
import { DataPersistence, NxModule } from '@nrwl/nx';
24+
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
2125

2226
@NgModule({
2327
declarations: [
@@ -39,7 +43,13 @@ import { WidgetsTotalComponent } from './widgets/widgets-total/widgets-total.com
3943
AppRoutingModule,
4044
AppMaterialModule,
4145
CommonDataModule,
42-
TotalsViewModule
46+
TotalsViewModule,
47+
NxModule.forRoot(),
48+
StoreModule.forRoot({}),
49+
EffectsModule.forRoot([]),
50+
StoreDevtoolsModule.instrument({
51+
maxAge: 5
52+
}),
4353
],
4454
bootstrap: [AppComponent]
4555
})
Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Component, OnInit } from '@angular/core';
22
import { Item, ItemsService } from '@workspace/common-data';
3+
import { ActionsSubject, select, Store } from '@ngrx/store';
4+
import { ItemsState } from '../../../../../libs/common-data/src/lib/state/items.reducer';
5+
import * as ItemsActions from '../../../../../libs/common-data/src/lib/state/items.actions';
6+
import { filter, tap } from 'rxjs/operators';
7+
import { ItemsActionTypes } from '../../../../../libs/common-data/src/lib/state/items.actions';
8+
import { selectAllItems } from '../../../../../libs/common-data/src/lib/state';
39

410
@Component({
511
selector: 'app-items',
@@ -10,13 +16,26 @@ export class ItemsComponent implements OnInit {
1016
items: Item[];
1117
currentItem: Item;
1218

13-
constructor(private itemsService: ItemsService) { }
19+
constructor(private itemsService: ItemsService, private store: Store<ItemsState>, private actions$: ActionsSubject) { }
1420

1521
ngOnInit() {
1622
this.getItems();
23+
this.handleActions();
1724
this.resetCurrentItem();
1825
}
1926

27+
handleActions() {
28+
this.actions$.pipe(
29+
filter(action =>
30+
action.type === ItemsActionTypes.AddItem
31+
|| action.type === ItemsActionTypes.UpdateItem
32+
|| action.type === ItemsActionTypes.DeleteItem
33+
),
34+
tap(_ => this.resetCurrentItem())
35+
)
36+
.subscribe();
37+
}
38+
2039
resetCurrentItem() {
2140
this.currentItem = { id: null, name: '', price: 0, description: '' };
2241
}
@@ -30,39 +49,21 @@ export class ItemsComponent implements OnInit {
3049
}
3150

3251
getItems() {
33-
this.itemsService.all()
34-
.subscribe((items: Item[]) => this.items = items);
52+
this.store.pipe(select(selectAllItems))
53+
.subscribe(items => this.items = items);
54+
55+
this.store.dispatch(new ItemsActions.LoadItems());
3556
}
3657

3758
saveItem(item) {
3859
if (!item.id) {
39-
this.createItem(item);
60+
this.store.dispatch(new ItemsActions.AddItem(item));
4061
} else {
41-
this.updateItem(item);
62+
this.store.dispatch(new ItemsActions.UpdateItem(item));
4263
}
4364
}
4465

45-
createItem(item) {
46-
this.itemsService.create(item)
47-
.subscribe(response => {
48-
this.getItems();
49-
this.resetCurrentItem();
50-
});
51-
}
52-
53-
updateItem(item) {
54-
this.itemsService.update(item)
55-
.subscribe(response => {
56-
this.getItems();
57-
this.resetCurrentItem();
58-
});
59-
}
60-
6166
deleteItem(item) {
62-
this.itemsService.delete(item)
63-
.subscribe(response => {
64-
this.getItems();
65-
this.resetCurrentItem();
66-
});
67+
this.store.dispatch(new ItemsActions.DeleteItem(item));
6768
}
6869
}

libs/common-data/src/lib/common-data.module.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,26 @@ import { CommonModule } from '@angular/common';
33
import { HttpClientModule } from '@angular/common/http';
44
import { ItemsService } from './items/items.service';
55
import { WidgetsService } from './widgets/widgets.service';
6+
import { StoreModule } from '@ngrx/store';
7+
import { EffectsModule } from '@ngrx/effects';
8+
import { itemsReducer, initialState as itemsInitialState } from './state/items.reducer';
9+
import { ItemsEffects } from './state/items.effects';
610

711
@NgModule({
812
imports: [
913
CommonModule,
10-
HttpClientModule
14+
HttpClientModule,
15+
StoreModule.forFeature('items', itemsReducer, { initialState: itemsInitialState }),
16+
EffectsModule.forFeature([ItemsEffects]),
1117
],
1218
providers: [
1319
ItemsService,
14-
WidgetsService
20+
WidgetsService,
21+
ItemsEffects
22+
],
23+
exports: [
24+
StoreModule,
25+
EffectsModule
1526
]
1627
})
1728

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
createSelector,
3+
createFeatureSelector,
4+
ActionReducerMap,
5+
} from '@ngrx/store';
6+
import * as fromItems from './items.reducer';
7+
8+
export interface ItemsState {
9+
items: fromItems.ItemsState;
10+
}
11+
12+
export const reducers: ActionReducerMap<ItemsState> = {
13+
items: fromItems.itemsReducer,
14+
};
15+
16+
export const selectItemItemsState = createFeatureSelector<fromItems.ItemsState>('items');
17+
18+
export const selectItemIds = createSelector(
19+
selectItemItemsState,
20+
fromItems.selectItemIds
21+
);
22+
export const selectItemEntities = createSelector(
23+
selectItemItemsState,
24+
fromItems.selectItemEntities
25+
);
26+
export const selectAllItems = createSelector(
27+
selectItemItemsState,
28+
fromItems.selectAllItems
29+
);
30+
export const selectItemTotal = createSelector(
31+
selectItemItemsState,
32+
fromItems.selectItemTotal
33+
);
34+
export const selectCurrentItemId = createSelector(
35+
selectItemItemsState,
36+
fromItems.getSelectedItemId
37+
);
38+
39+
export const selectCurrentItem = createSelector(
40+
selectItemEntities,
41+
selectCurrentItemId,
42+
(itemEntities, itemId) => itemEntities[itemId]
43+
);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Action } from '@ngrx/store';
2+
import { Item } from '@workspace/common-data';
3+
4+
export enum ItemsActionTypes {
5+
ItemsAction = '[Items] Action',
6+
LoadItems = '[Items] Load Data',
7+
ItemsLoaded = '[Items] Data Loaded',
8+
AddItem = '[Items] Add Data',
9+
ItemAdded = '[Items] Data Added',
10+
UpdateItem = '[Items] Update Data',
11+
ItemUpdated = '[Items] Data Updated',
12+
DeleteItem = '[Items] Delete Data',
13+
ItemDeleted = '[Items] Data Deleted',
14+
}
15+
16+
export class Items implements Action {
17+
readonly type = ItemsActionTypes.ItemsAction;
18+
}
19+
export class LoadItems implements Action {
20+
readonly type = ItemsActionTypes.LoadItems;
21+
constructor() {}
22+
}
23+
24+
export class ItemsLoaded implements Action {
25+
readonly type = ItemsActionTypes.ItemsLoaded;
26+
constructor(public payload: Item[]) {}
27+
}
28+
29+
export class AddItem implements Action {
30+
readonly type = ItemsActionTypes.AddItem;
31+
constructor(public payload: Item) {}
32+
}
33+
34+
export class ItemAdded implements Action {
35+
readonly type = ItemsActionTypes.ItemAdded;
36+
constructor(public payload: Item) {}
37+
}
38+
39+
export class UpdateItem implements Action {
40+
readonly type = ItemsActionTypes.UpdateItem;
41+
constructor(public payload: Item) {}
42+
}
43+
44+
export class ItemUpdated implements Action {
45+
readonly type = ItemsActionTypes.ItemUpdated;
46+
constructor(public payload: Item) {}
47+
}
48+
49+
export class DeleteItem implements Action {
50+
readonly type = ItemsActionTypes.DeleteItem;
51+
constructor(public payload: Item) {}
52+
}
53+
54+
export class ItemDeleted implements Action {
55+
readonly type = ItemsActionTypes.ItemDeleted;
56+
constructor(public payload: Item) {}
57+
}
58+
59+
export type ItemsActions = Items | LoadItems | ItemsLoaded | AddItem | ItemAdded | UpdateItem | ItemUpdated | DeleteItem | ItemDeleted;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { StoreModule } from '@ngrx/store';
3+
import { provideMockActions } from '@ngrx/effects/testing';
4+
import { DataPersistence } from '@nrwl/nx';
5+
import { hot } from '@nrwl/nx/testing';
6+
7+
import { ItemsEffects } from './items.effects';
8+
import { LoadItems, ItemsLoaded } from './items.actions';
9+
10+
import { Observable } from 'rxjs';
11+
12+
describe('ItemsEffects', () => {
13+
let actions$: Observable<any>;
14+
let effects$: ItemsEffects;
15+
16+
beforeEach(() => {
17+
TestBed.configureTestingModule({
18+
imports: [StoreModule.forRoot({})],
19+
providers: [
20+
ItemsEffects,
21+
DataPersistence,
22+
provideMockActions(() => actions$)
23+
]
24+
});
25+
26+
effects$ = TestBed.get(ItemsEffects);
27+
});
28+
29+
describe('someEffect', () => {
30+
it('should work', () => {
31+
actions$ = hot('-a-|', { a: new LoadItems({}) });
32+
expect(effects$.loadItems$).toBeObservable(
33+
hot('-a-|', { a: new ItemsLoaded({}) })
34+
);
35+
});
36+
});
37+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Injectable } from '@angular/core';
2+
import { Actions, Effect } from '@ngrx/effects';
3+
import {
4+
ItemsActions,
5+
ItemsActionTypes,
6+
LoadItems,
7+
ItemsLoaded, ItemUpdated, ItemDeleted, AddItem, UpdateItem, DeleteItem, ItemAdded
8+
} from './items.actions';
9+
import { ItemsState } from './items.reducer';
10+
import { DataPersistence } from '@nrwl/nx';
11+
import { ItemsService } from '../items/items.service';
12+
import { map } from 'rxjs/operators';
13+
import { Item } from '../items/item.model';
14+
15+
@Injectable()
16+
export class ItemsEffects {
17+
@Effect() effect$ = this.actions$.ofType(ItemsActionTypes.ItemsAction);
18+
19+
@Effect()
20+
loadItems$ = this.dataPersistence.fetch(ItemsActionTypes.LoadItems, {
21+
run: (action: LoadItems, state: ItemsState) => {
22+
return this.itemsService.all().pipe(map((res: Item[]) => new ItemsLoaded(res)))
23+
},
24+
25+
onError: (action: LoadItems, error) => {
26+
console.error('Error', error);
27+
}
28+
});
29+
30+
@Effect()
31+
addItem$ = this.dataPersistence.pessimisticUpdate(ItemsActionTypes.AddItem, {
32+
run: (action: AddItem, state: ItemsState) => {
33+
return this.itemsService.create(action.payload).pipe(map((res: Item) => new ItemAdded(res)))
34+
},
35+
36+
onError: (action: AddItem, error) => {
37+
console.error('Error', error);
38+
}
39+
});
40+
41+
@Effect()
42+
updateItem$ = this.dataPersistence.pessimisticUpdate(ItemsActionTypes.UpdateItem, {
43+
run: (action: UpdateItem, state: ItemsState) => {
44+
return this.itemsService.update(action.payload).pipe(map((res: Item) => new ItemUpdated(res)))
45+
},
46+
47+
onError: (action: UpdateItem, error) => {
48+
console.error('Error', error);
49+
}
50+
});
51+
52+
@Effect()
53+
deleteItem$ = this.dataPersistence.pessimisticUpdate(ItemsActionTypes.DeleteItem, {
54+
run: (action: DeleteItem, state: ItemsState) => {
55+
return this.itemsService.delete(action.payload).pipe(map(_ => new ItemDeleted(action.payload)))
56+
},
57+
58+
onError: (action: DeleteItem, error) => {
59+
console.error('Error', error);
60+
}
61+
});
62+
63+
constructor(
64+
private actions$: Actions,
65+
private dataPersistence: DataPersistence<ItemsState>,
66+
private itemsService: ItemsService
67+
) {}
68+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ItemsLoaded } from './items.actions';
2+
import { itemsReducer, initialState } from './items.reducer';
3+
4+
describe('itemsReducer', () => {
5+
it('should work', () => {
6+
const action: ItemsLoaded = new ItemsLoaded({});
7+
const actual = itemsReducer(initialState, action);
8+
expect(actual).toEqual({});
9+
});
10+
});

0 commit comments

Comments
 (0)