| @@ -6,6 +6,7 @@ import { DataStorageService } from "../../shared/data-storage.service"; | |||||
| import * as fromApp from '../../ngrx/app.reducers'; | import * as fromApp from '../../ngrx/app.reducers'; | ||||
| import * as fromAuth from '../../auth/ngrx/auth.reducers'; | import * as fromAuth from '../../auth/ngrx/auth.reducers'; | ||||
| import * as AuthActions from '../../auth/ngrx/auth.actions'; | import * as AuthActions from '../../auth/ngrx/auth.actions'; | ||||
| import * as RecipeActions from '../../recipes/ngrx/recipe.actions'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-header', | selector: 'app-header', | ||||
| @@ -29,7 +30,7 @@ export class HeaderComponent implements OnInit { | |||||
| } | } | ||||
| onFetchData() { | onFetchData() { | ||||
| this.dataStorageService.fetchRecipes(); | |||||
| this.store.dispatch(new RecipeActions.FetchRecipes()); | |||||
| } | } | ||||
| onLogout() { | onLogout() { | ||||
| @@ -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; | |||||
| @@ -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<Recipe[]>(this.baseUrl + 'recipes.json'); | |||||
| }) | |||||
| .map((recipes) => { | |||||
| return { | |||||
| type: RecipeActions.SET_RECIPES, | |||||
| payload: recipes | |||||
| }; | |||||
| }); | |||||
| constructor(private actions$: Actions, | |||||
| private httpClient: HttpClient) {} | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -1,11 +1,11 @@ | |||||
| <div class="row"> | <div class="row"> | ||||
| <div class="col-xs-12"> | <div class="col-xs-12"> | ||||
| <img [src]="recipe.imagePath" alt="" class="img-responsive" style="max-height: 300px;"> | |||||
| <img [src]="(recipeState | async).recipes[id].imagePath" alt="" class="img-responsive" style="max-height: 300px;"> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="row"> | <div class="row"> | ||||
| <div class="col-xs-12"> | <div class="col-xs-12"> | ||||
| <h1>{{ recipe.name }}</h1> | |||||
| <h1>{{ (recipeState | async).recipes[id].name }}</h1> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="row"> | <div class="row"> | ||||
| @@ -26,7 +26,7 @@ | |||||
| </div> | </div> | ||||
| <div class="row"> | <div class="row"> | ||||
| <div class="col-xs-12"> | <div class="col-xs-12"> | ||||
| {{ recipe.description }} | |||||
| {{ (recipeState | async).recipes[id].description }} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="row"> | <div class="row"> | ||||
| @@ -34,8 +34,8 @@ | |||||
| <ul class="list-group"> | <ul class="list-group"> | ||||
| <li | <li | ||||
| class="list-group-item" | class="list-group-item" | ||||
| *ngFor="let ingredient of recipe.ingredients"> | |||||
| {{ ingredient.name }} - {{ ingredient.amount }} | |||||
| *ngFor="let ingredient of (recipeState | async).recipes[id].ingredients"> | |||||
| {{ (recipeState | async).recipes[id].name }} - {{ (recipeState | async).recipes[id].amount }} | |||||
| </li> | </li> | ||||
| </ul> | </ul> | ||||
| </div> | </div> | ||||
| @@ -1,11 +1,13 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||
| import { Router, ActivatedRoute, Params } from '@angular/router'; | import { Router, ActivatedRoute, Params } from '@angular/router'; | ||||
| import { Store } from '@ngrx/store'; | import { Store } from '@ngrx/store'; | ||||
| import { Observable } from 'rxjs/Observable'; | |||||
| import { Recipe } from '../recipe.model'; | import { Recipe } from '../recipe.model'; | ||||
| import { RecipeService } from '../recipe.service'; | |||||
| import * as ShoppingListActions from '../../shopping-list/ngrx/shopping-list.actions'; | import * as ShoppingListActions from '../../shopping-list/ngrx/shopping-list.actions'; | ||||
| import * as fromApp from '../../ngrx/app.reducers'; | import * as fromApp from '../../ngrx/app.reducers'; | ||||
| import * as fromRecipe from '../ngrx/recipe.reducers'; | |||||
| import * as RecipeACtions from '../ngrx/recipe.actions'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-recipe-detail', | selector: 'app-recipe-detail', | ||||
| @@ -13,25 +15,29 @@ import * as fromApp from '../../ngrx/app.reducers'; | |||||
| styleUrls: ['./recipe-detail.component.css'] | styleUrls: ['./recipe-detail.component.css'] | ||||
| }) | }) | ||||
| export class RecipeDetailComponent implements OnInit { | export class RecipeDetailComponent implements OnInit { | ||||
| recipe: Recipe; | |||||
| recipeState: Observable<fromRecipe.State>; | |||||
| id: number; | id: number; | ||||
| constructor(private recipeService: RecipeService, | |||||
| private route: ActivatedRoute, | |||||
| constructor(private route: ActivatedRoute, | |||||
| private router: Router, | private router: Router, | ||||
| private store: Store<fromApp.AppState>) { } | |||||
| private store: Store<fromRecipe.FeatureState>) { } | |||||
| ngOnInit() { | ngOnInit() { | ||||
| this.route.params.subscribe( | this.route.params.subscribe( | ||||
| (params: Params) => { | (params: Params) => { | ||||
| this.id = +params['id']; | this.id = +params['id']; | ||||
| this.recipe = this.recipeService.getRecipe(this.id); | |||||
| this.recipeState = this.store.select('recipes'); | |||||
| } | } | ||||
| ); | ); | ||||
| } | } | ||||
| onAddToShoppingList() { | 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() { | onEditRecipe() { | ||||
| @@ -39,7 +45,7 @@ export class RecipeDetailComponent implements OnInit { | |||||
| } | } | ||||
| onDelete() { | onDelete() { | ||||
| this.recipeService.delteRecipe(this.id); | |||||
| this.store.dispatch(new RecipeACtions.DeleteRecipe(this.id)); | |||||
| this.router.navigate(['/recipes']); | this.router.navigate(['/recipes']); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,8 +1,10 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, Params, Router } from '@angular/router'; | import { ActivatedRoute, Params, Router } from '@angular/router'; | ||||
| import { FormArray, FormGroup, FormControl, Validators } from '@angular/forms'; | 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({ | @Component({ | ||||
| selector: 'app-recipe-edit', | selector: 'app-recipe-edit', | ||||
| @@ -15,8 +17,8 @@ export class RecipeEditComponent implements OnInit { | |||||
| recipeForm: FormGroup | recipeForm: FormGroup | ||||
| constructor(private route: ActivatedRoute, | constructor(private route: ActivatedRoute, | ||||
| private recipeService: RecipeService, | |||||
| private router: Router) { } | |||||
| private router: Router, | |||||
| private store: Store<fromRecipe.FeatureState>) { } | |||||
| ngOnInit() { | ngOnInit() { | ||||
| this.route.params.subscribe( | this.route.params.subscribe( | ||||
| @@ -29,14 +31,10 @@ export class RecipeEditComponent implements OnInit { | |||||
| } | } | ||||
| onSubmit() { | onSubmit() { | ||||
| /* const newRecipe = new Recipe(this.recipeForm.value['name'], | |||||
| this.recipeForm.valid['description'], | |||||
| this.recipeForm.valid['imagePath'], | |||||
| this.recipeForm.value['ingredients']); */ | |||||
| if (this.editMode) { | 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 { | } else { | ||||
| this.recipeService.addRecipe(this.recipeForm.value); | |||||
| this.store.dispatch(new RecipeActions.AddRecipe(this.recipeForm.value)); | |||||
| } | } | ||||
| this.router.navigate(['../'], {relativeTo: this.route}); | this.router.navigate(['../'], {relativeTo: this.route}); | ||||
| } | } | ||||
| @@ -65,20 +63,23 @@ export class RecipeEditComponent implements OnInit { | |||||
| let recipeIngredients = new FormArray([]); | let recipeIngredients = new FormArray([]); | ||||
| if (this.editMode) { | 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({ | this.recipeForm = new FormGroup({ | ||||
| @@ -7,7 +7,7 @@ | |||||
| <div class="row"> | <div class="row"> | ||||
| <div class="col-xs-12"> | <div class="col-xs-12"> | ||||
| <app-recipe-item | <app-recipe-item | ||||
| *ngFor="let recipeElement of recipes; let i = index" | |||||
| *ngFor="let recipeElement of (recipeState | async).recipes; let i = index" | |||||
| [recipe]="recipeElement" | [recipe]="recipeElement" | ||||
| [index]="i"></app-recipe-item> | [index]="i"></app-recipe-item> | ||||
| </div> | </div> | ||||
| @@ -1,38 +1,30 @@ | |||||
| import { Component, OnInit, OnDestroy } from '@angular/core'; | |||||
| import { Component, OnInit } from '@angular/core'; | |||||
| import { Router, ActivatedRoute } from '@angular/router'; | import { Router, ActivatedRoute } from '@angular/router'; | ||||
| import { Store } from '@ngrx/store'; | |||||
| import { Observable } from 'rxjs/Observable'; | |||||
| import { Recipe } from '../recipe.model'; | import { Recipe } from '../recipe.model'; | ||||
| import { RecipeService } from '../recipe.service'; | import { RecipeService } from '../recipe.service'; | ||||
| import { Subscription } from 'rxjs/Subscription'; | |||||
| import * as fromRecipe from '../ngrx/recipe.reducers'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-recipe-list', | selector: 'app-recipe-list', | ||||
| templateUrl: './recipe-list.component.html', | templateUrl: './recipe-list.component.html', | ||||
| styleUrls: ['./recipe-list.component.css'] | styleUrls: ['./recipe-list.component.css'] | ||||
| }) | }) | ||||
| export class RecipeListComponent implements OnInit, OnDestroy { | |||||
| subscription: Subscription; | |||||
| recipes: Recipe[]; | |||||
| export class RecipeListComponent implements OnInit { | |||||
| recipeState: Observable<fromRecipe.State>; | |||||
| constructor(private recipeService: RecipeService, | |||||
| private router: Router, | |||||
| private route: ActivatedRoute) { } | |||||
| constructor(private router: Router, | |||||
| private route: ActivatedRoute, | |||||
| private store: Store<fromRecipe.FeatureState>) { } | |||||
| ngOnInit() { | ngOnInit() { | ||||
| this.subscription = this.recipeService.recipesChanged.subscribe( | |||||
| (recipes: Recipe[]) => { | |||||
| this.recipes = recipes; | |||||
| } | |||||
| ); | |||||
| this.recipes = this.recipeService.getRecipes(); | |||||
| this.recipeState = this.store.select('recipes'); | |||||
| } | } | ||||
| onNewRecipe() { | onNewRecipe() { | ||||
| this.router.navigate(['new'], {relativeTo: this.route}); | this.router.navigate(['new'], {relativeTo: this.route}); | ||||
| } | } | ||||
| ngOnDestroy() { | |||||
| this.subscription.unsubscribe(); | |||||
| } | |||||
| } | } | ||||
| @@ -16,23 +16,4 @@ export class RecipeService { | |||||
| getRecipes() { | getRecipes() { | ||||
| return this.recipes.slice(); | 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()); | |||||
| } | |||||
| } | } | ||||
| @@ -1,6 +1,8 @@ | |||||
| import { NgModule } from "@angular/core"; | import { NgModule } from "@angular/core"; | ||||
| import { ReactiveFormsModule } from "@angular/forms"; | import { ReactiveFormsModule } from "@angular/forms"; | ||||
| import { CommonModule } from "@angular/common"; | import { CommonModule } from "@angular/common"; | ||||
| import { StoreModule } from "@ngrx/store"; | |||||
| import { EffectsModule } from "@ngrx/effects"; | |||||
| import { RecipesComponent } from "./recipes.component"; | import { RecipesComponent } from "./recipes.component"; | ||||
| import { RecipeStartComponent } from "./recipe-start/recipe-start.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 { RecipeItemComponent } from "./recipe-list/recipe-item/recipe-item.component"; | ||||
| import { RecipesRoutingModule } from "./recipes-routing.module"; | import { RecipesRoutingModule } from "./recipes-routing.module"; | ||||
| import { SharedModule } from "../shared/shared.module"; | import { SharedModule } from "../shared/shared.module"; | ||||
| import { recipeReducer } from "./ngrx/recipe.reducers"; | |||||
| import { RecipeEffects } from "./ngrx/recipe.effects"; | |||||
| @NgModule({ | @NgModule({ | ||||
| declarations: [ | declarations: [ | ||||
| @@ -25,7 +28,9 @@ import { SharedModule } from "../shared/shared.module"; | |||||
| CommonModule, | CommonModule, | ||||
| ReactiveFormsModule, | ReactiveFormsModule, | ||||
| RecipesRoutingModule, | RecipesRoutingModule, | ||||
| SharedModule | |||||
| SharedModule, | |||||
| StoreModule.forFeature('recipes', recipeReducer), | |||||
| EffectsModule.forFeature([RecipeEffects]) | |||||
| ] | ] | ||||
| }) | }) | ||||
| export class RecipesModule { | export class RecipesModule { | ||||
| @@ -1,5 +1,5 @@ | |||||
| import { Injectable } from '@angular/core'; | 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 { RecipeService } from '../recipes/recipe.service'; | ||||
| import { Recipe } from '../recipes/recipe.model'; | import { Recipe } from '../recipes/recipe.model'; | ||||