| @@ -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() { | |||
| @@ -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="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 class="row"> | |||
| <div class="col-xs-12"> | |||
| <h1>{{ recipe.name }}</h1> | |||
| <h1>{{ (recipeState | async).recipes[id].name }}</h1> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| @@ -26,7 +26,7 @@ | |||
| </div> | |||
| <div class="row"> | |||
| <div class="col-xs-12"> | |||
| {{ recipe.description }} | |||
| {{ (recipeState | async).recipes[id].description }} | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| @@ -34,8 +34,8 @@ | |||
| <ul class="list-group"> | |||
| <li | |||
| 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> | |||
| </ul> | |||
| </div> | |||
| @@ -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<fromRecipe.State>; | |||
| id: number; | |||
| constructor(private recipeService: RecipeService, | |||
| private route: ActivatedRoute, | |||
| constructor(private route: ActivatedRoute, | |||
| private router: Router, | |||
| private store: Store<fromApp.AppState>) { } | |||
| private store: Store<fromRecipe.FeatureState>) { } | |||
| 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']); | |||
| } | |||
| } | |||
| @@ -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<fromRecipe.FeatureState>) { } | |||
| 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({ | |||
| @@ -7,7 +7,7 @@ | |||
| <div class="row"> | |||
| <div class="col-xs-12"> | |||
| <app-recipe-item | |||
| *ngFor="let recipeElement of recipes; let i = index" | |||
| *ngFor="let recipeElement of (recipeState | async).recipes; let i = index" | |||
| [recipe]="recipeElement" | |||
| [index]="i"></app-recipe-item> | |||
| </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 { 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<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() { | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -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()); | |||
| } | |||
| } | |||
| @@ -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 { | |||
| @@ -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'; | |||