From 1bdf8d41315274208e25cce02359f6fc63d181c1 Mon Sep 17 00:00:00 2001 From: Nils Dittberner Date: Mon, 4 Dec 2017 13:29:52 +0100 Subject: [PATCH] 24-341 Fetching data with ngrx --- src/app/core/header/header.component.ts | 3 +- src/app/recipes/ngrx/recipe.actions.ts | 43 ++++++++++++++++++ src/app/recipes/ngrx/recipe.effects.ts | 28 ++++++++++++ src/app/recipes/ngrx/recipe.reducers.ts | 52 ++++++++++++++++++++++ .../recipe-detail/recipe-detail.component.html | 10 ++--- .../recipe-detail/recipe-detail.component.ts | 22 +++++---- .../recipes/recipe-edit/recipe-edit.component.ts | 47 +++++++++---------- .../recipes/recipe-list/recipe-list.component.html | 2 +- .../recipes/recipe-list/recipe-list.component.ts | 30 +++++-------- src/app/recipes/recipe.service.ts | 19 -------- src/app/recipes/recipes.module.ts | 9 +++- src/app/shared/data-storage.service.ts | 2 +- 12 files changed, 188 insertions(+), 79 deletions(-) create mode 100644 src/app/recipes/ngrx/recipe.actions.ts create mode 100644 src/app/recipes/ngrx/recipe.effects.ts create mode 100644 src/app/recipes/ngrx/recipe.reducers.ts diff --git a/src/app/core/header/header.component.ts b/src/app/core/header/header.component.ts index 0041198..442d7e6 100644 --- a/src/app/core/header/header.component.ts +++ b/src/app/core/header/header.component.ts @@ -6,6 +6,7 @@ import { DataStorageService } from "../../shared/data-storage.service"; import * as fromApp from '../../ngrx/app.reducers'; import * as fromAuth from '../../auth/ngrx/auth.reducers'; import * as AuthActions from '../../auth/ngrx/auth.actions'; +import * as RecipeActions from '../../recipes/ngrx/recipe.actions'; @Component({ selector: 'app-header', @@ -29,7 +30,7 @@ export class HeaderComponent implements OnInit { } onFetchData() { - this.dataStorageService.fetchRecipes(); + this.store.dispatch(new RecipeActions.FetchRecipes()); } onLogout() { diff --git a/src/app/recipes/ngrx/recipe.actions.ts b/src/app/recipes/ngrx/recipe.actions.ts new file mode 100644 index 0000000..0ad902b --- /dev/null +++ b/src/app/recipes/ngrx/recipe.actions.ts @@ -0,0 +1,43 @@ +import { Action } from '@ngrx/store'; +import { Recipe } from '../recipe.model'; + +export const SET_RECIPES = 'SET_RECIPES'; +export const ADD_RECIPE = 'ADD_RECIPE'; +export const UPDATE_RECIPE = 'UPDATE_RECIPE'; +export const DELETE_RECIPE = 'DELETE_RECIPE'; +export const STORE_RECIPES = 'STORE_RECIPES'; +export const FETCH_RECIPES = 'FETCH_RECIPES'; + +export class SetRecipes implements Action { + readonly type = SET_RECIPES; + + constructor(public payload: Recipe[]) {} +} + +export class AddRecipe implements Action { + readonly type = ADD_RECIPE; + + constructor(public payload: Recipe) {} +} + +export class UpdateRecipe implements Action { + readonly type = UPDATE_RECIPE; + + constructor(public payload: {index: number, newRecipe: Recipe}) {} +} + +export class DeleteRecipe implements Action { + readonly type = DELETE_RECIPE; + + constructor(public payload: number) {} +} + +export class StoreRecipes implements Action { + readonly type = STORE_RECIPES; +} + +export class FetchRecipes implements Action { + readonly type = FETCH_RECIPES; +} + +export type RecipeActions = SetRecipes | AddRecipe | UpdateRecipe | DeleteRecipe | StoreRecipes | FetchRecipes; \ No newline at end of file diff --git a/src/app/recipes/ngrx/recipe.effects.ts b/src/app/recipes/ngrx/recipe.effects.ts new file mode 100644 index 0000000..29ad6a2 --- /dev/null +++ b/src/app/recipes/ngrx/recipe.effects.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { Actions, Effect } from "@ngrx/effects"; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; +import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'; + +import * as RecipeActions from '../ngrx/recipe.actions'; +import { Recipe } from "../recipe.model"; + +@Injectable() +export class RecipeEffects { + readonly baseUrl: string = 'https://my-recipe-book-cb837.firebaseio.com/'; + + @Effect() + recipeFetch = this.actions$.ofType(RecipeActions.FETCH_RECIPES) + .switchMap((action: RecipeActions.FetchRecipes) => { + return this.httpClient.get(this.baseUrl + 'recipes.json'); + }) + .map((recipes) => { + return { + type: RecipeActions.SET_RECIPES, + payload: recipes + }; + }); + + constructor(private actions$: Actions, + private httpClient: HttpClient) {} +} \ No newline at end of file diff --git a/src/app/recipes/ngrx/recipe.reducers.ts b/src/app/recipes/ngrx/recipe.reducers.ts new file mode 100644 index 0000000..31afe73 --- /dev/null +++ b/src/app/recipes/ngrx/recipe.reducers.ts @@ -0,0 +1,52 @@ +import { Recipe } from "../recipe.model"; +import * as RecipeActions from "./recipe.actions"; +import * as fromApp from "../../ngrx/app.reducers"; + +// RecipeState, only for a feature of the app, not the whole app +export interface FeatureState extends fromApp.AppState { + recipes: State +} + +export interface State { + recipes: Recipe[]; +} + +const initialState: State = { + recipes: [new Recipe("Foo", "Bar", "image.jpg", [])] +}; + +export function recipeReducer(state = initialState, action: RecipeActions.RecipeActions) { + switch(action.type) { + case RecipeActions.SET_RECIPES: + return { + ...state, + recipes: [...action.payload] + }; + case RecipeActions.ADD_RECIPE: + return { + ...state, + recipes: [...state.recipes, action.payload] + }; + case RecipeActions.UPDATE_RECIPE: + const recipe = state.recipes[action.payload.index]; + const newRecipe = { + ...recipe, + ...action.payload.newRecipe + }; + const recipes = [...state.recipes]; + recipes[action.payload.index] = newRecipe; + return { + ...state, + recipes: recipes + } + case RecipeActions.DELETE_RECIPE: + const oldRecipes = [...state.recipes]; + oldRecipes.splice(action.payload, 1); + return { + ...state, + recipes: oldRecipes + } + default: + return state; + } +} \ No newline at end of file diff --git a/src/app/recipes/recipe-detail/recipe-detail.component.html b/src/app/recipes/recipe-detail/recipe-detail.component.html index ea90ff7..037e963 100644 --- a/src/app/recipes/recipe-detail/recipe-detail.component.html +++ b/src/app/recipes/recipe-detail/recipe-detail.component.html @@ -1,11 +1,11 @@
- +
-

{{ recipe.name }}

+

{{ (recipeState | async).recipes[id].name }}

@@ -26,7 +26,7 @@
- {{ recipe.description }} + {{ (recipeState | async).recipes[id].description }}
@@ -34,8 +34,8 @@
  • - {{ ingredient.name }} - {{ ingredient.amount }} + *ngFor="let ingredient of (recipeState | async).recipes[id].ingredients"> + {{ (recipeState | async).recipes[id].name }} - {{ (recipeState | async).recipes[id].amount }}
diff --git a/src/app/recipes/recipe-detail/recipe-detail.component.ts b/src/app/recipes/recipe-detail/recipe-detail.component.ts index 5632a38..4f29d52 100644 --- a/src/app/recipes/recipe-detail/recipe-detail.component.ts +++ b/src/app/recipes/recipe-detail/recipe-detail.component.ts @@ -1,11 +1,13 @@ import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; import { Recipe } from '../recipe.model'; -import { RecipeService } from '../recipe.service'; import * as ShoppingListActions from '../../shopping-list/ngrx/shopping-list.actions'; import * as fromApp from '../../ngrx/app.reducers'; +import * as fromRecipe from '../ngrx/recipe.reducers'; +import * as RecipeACtions from '../ngrx/recipe.actions'; @Component({ selector: 'app-recipe-detail', @@ -13,25 +15,29 @@ import * as fromApp from '../../ngrx/app.reducers'; styleUrls: ['./recipe-detail.component.css'] }) export class RecipeDetailComponent implements OnInit { - recipe: Recipe; + recipeState: Observable; id: number; - constructor(private recipeService: RecipeService, - private route: ActivatedRoute, + constructor(private route: ActivatedRoute, private router: Router, - private store: Store) { } + private store: Store) { } ngOnInit() { this.route.params.subscribe( (params: Params) => { this.id = +params['id']; - this.recipe = this.recipeService.getRecipe(this.id); + this.recipeState = this.store.select('recipes'); } ); } onAddToShoppingList() { - this.store.dispatch(new ShoppingListActions.AddIngredients(this.recipe.ingredients)); + this.store.select('recipes') + .take(1) + .subscribe((recipeState: fromRecipe.State) => { + this.store.dispatch(new ShoppingListActions.AddIngredients(recipeState.recipes[this.id].ingredients)); + }); + } onEditRecipe() { @@ -39,7 +45,7 @@ export class RecipeDetailComponent implements OnInit { } onDelete() { - this.recipeService.delteRecipe(this.id); + this.store.dispatch(new RecipeACtions.DeleteRecipe(this.id)); this.router.navigate(['/recipes']); } } diff --git a/src/app/recipes/recipe-edit/recipe-edit.component.ts b/src/app/recipes/recipe-edit/recipe-edit.component.ts index 42d30f2..9f4dad5 100644 --- a/src/app/recipes/recipe-edit/recipe-edit.component.ts +++ b/src/app/recipes/recipe-edit/recipe-edit.component.ts @@ -1,8 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { FormArray, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; -import { RecipeService } from '../recipe.service'; +import * as fromRecipe from '../ngrx/recipe.reducers'; +import * as RecipeActions from '../ngrx/recipe.actions'; @Component({ selector: 'app-recipe-edit', @@ -15,8 +17,8 @@ export class RecipeEditComponent implements OnInit { recipeForm: FormGroup constructor(private route: ActivatedRoute, - private recipeService: RecipeService, - private router: Router) { } + private router: Router, + private store: Store) { } ngOnInit() { this.route.params.subscribe( @@ -29,14 +31,10 @@ export class RecipeEditComponent implements OnInit { } onSubmit() { - /* const newRecipe = new Recipe(this.recipeForm.value['name'], - this.recipeForm.valid['description'], - this.recipeForm.valid['imagePath'], - this.recipeForm.value['ingredients']); */ if (this.editMode) { - this.recipeService.updateRecpe(this.id, this.recipeForm.value); + this.store.dispatch(new RecipeActions.UpdateRecipe({index: this.id, newRecipe: this.recipeForm.value})); } else { - this.recipeService.addRecipe(this.recipeForm.value); + this.store.dispatch(new RecipeActions.AddRecipe(this.recipeForm.value)); } this.router.navigate(['../'], {relativeTo: this.route}); } @@ -65,20 +63,23 @@ export class RecipeEditComponent implements OnInit { let recipeIngredients = new FormArray([]); if (this.editMode) { - const recipe = this.recipeService.getRecipe(this.id); - recipeName = recipe.name; - recipeImagePath = recipe.imagePath; - recipeDescription = recipe.description; - if (recipe['ingredients']) { - for (let ingredient of recipe.ingredients) { - recipeIngredients.push( - new FormGroup({ - 'name': new FormControl(ingredient.name, Validators.required), - 'amount': new FormControl(ingredient.amount, [Validators.required, Validators.pattern(/^[1-9]+[0-9]*$/)]) - }) - ); - } - } + this.store.select('recipes') + .take(1) + .subscribe((recipeState: fromRecipe.State) => { + const recipe = recipeState.recipes[this.id];recipeName = recipe.name; + recipeImagePath = recipe.imagePath; + recipeDescription = recipe.description; + if (recipe['ingredients']) { + for (let ingredient of recipe.ingredients) { + recipeIngredients.push( + new FormGroup({ + 'name': new FormControl(ingredient.name, Validators.required), + 'amount': new FormControl(ingredient.amount, [Validators.required, Validators.pattern(/^[1-9]+[0-9]*$/)]) + }) + ); + } + } + }) } this.recipeForm = new FormGroup({ diff --git a/src/app/recipes/recipe-list/recipe-list.component.html b/src/app/recipes/recipe-list/recipe-list.component.html index 8b7ecc0..e0b73c5 100644 --- a/src/app/recipes/recipe-list/recipe-list.component.html +++ b/src/app/recipes/recipe-list/recipe-list.component.html @@ -7,7 +7,7 @@
diff --git a/src/app/recipes/recipe-list/recipe-list.component.ts b/src/app/recipes/recipe-list/recipe-list.component.ts index 2fa3176..a849cd2 100644 --- a/src/app/recipes/recipe-list/recipe-list.component.ts +++ b/src/app/recipes/recipe-list/recipe-list.component.ts @@ -1,38 +1,30 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; + import { Recipe } from '../recipe.model'; import { RecipeService } from '../recipe.service'; -import { Subscription } from 'rxjs/Subscription'; - +import * as fromRecipe from '../ngrx/recipe.reducers'; @Component({ selector: 'app-recipe-list', templateUrl: './recipe-list.component.html', styleUrls: ['./recipe-list.component.css'] }) -export class RecipeListComponent implements OnInit, OnDestroy { - subscription: Subscription; - recipes: Recipe[]; +export class RecipeListComponent implements OnInit { + recipeState: Observable; - constructor(private recipeService: RecipeService, - private router: Router, - private route: ActivatedRoute) { } + constructor(private router: Router, + private route: ActivatedRoute, + private store: Store) { } ngOnInit() { - this.subscription = this.recipeService.recipesChanged.subscribe( - (recipes: Recipe[]) => { - this.recipes = recipes; - } - ); - this.recipes = this.recipeService.getRecipes(); + this.recipeState = this.store.select('recipes'); } onNewRecipe() { this.router.navigate(['new'], {relativeTo: this.route}); } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } } diff --git a/src/app/recipes/recipe.service.ts b/src/app/recipes/recipe.service.ts index cecd1ae..6b8c0e5 100644 --- a/src/app/recipes/recipe.service.ts +++ b/src/app/recipes/recipe.service.ts @@ -16,23 +16,4 @@ export class RecipeService { getRecipes() { return this.recipes.slice(); } - - getRecipe(index: number) { - return this.recipes[index]; - } - - addRecipe(recipe: Recipe) { - this.recipes.push(recipe); - this.recipesChanged.next(this.recipes.slice()); - } - - updateRecpe(index: number, newRecipe: Recipe) { - this.recipes[index] = newRecipe; - this.recipesChanged.next(this.recipes.slice()); - } - - delteRecipe(index: number) { - this.recipes.splice(index, 1); - this.recipesChanged.next(this.recipes.slice()); - } } \ No newline at end of file diff --git a/src/app/recipes/recipes.module.ts b/src/app/recipes/recipes.module.ts index 5a43ea2..518693e 100644 --- a/src/app/recipes/recipes.module.ts +++ b/src/app/recipes/recipes.module.ts @@ -1,6 +1,8 @@ import { NgModule } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; import { CommonModule } from "@angular/common"; +import { StoreModule } from "@ngrx/store"; +import { EffectsModule } from "@ngrx/effects"; import { RecipesComponent } from "./recipes.component"; import { RecipeStartComponent } from "./recipe-start/recipe-start.component"; @@ -10,7 +12,8 @@ import { RecipeDetailComponent } from "./recipe-detail/recipe-detail.component"; import { RecipeItemComponent } from "./recipe-list/recipe-item/recipe-item.component"; import { RecipesRoutingModule } from "./recipes-routing.module"; import { SharedModule } from "../shared/shared.module"; - +import { recipeReducer } from "./ngrx/recipe.reducers"; +import { RecipeEffects } from "./ngrx/recipe.effects"; @NgModule({ declarations: [ @@ -25,7 +28,9 @@ import { SharedModule } from "../shared/shared.module"; CommonModule, ReactiveFormsModule, RecipesRoutingModule, - SharedModule + SharedModule, + StoreModule.forFeature('recipes', recipeReducer), + EffectsModule.forFeature([RecipeEffects]) ] }) export class RecipesModule { diff --git a/src/app/shared/data-storage.service.ts b/src/app/shared/data-storage.service.ts index c21e898..20ec663 100644 --- a/src/app/shared/data-storage.service.ts +++ b/src/app/shared/data-storage.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' +import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'; import { RecipeService } from '../recipes/recipe.service'; import { Recipe } from '../recipes/recipe.model';