| @@ -336,6 +336,26 @@ | |||
| "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.4.tgz", | |||
| "integrity": "sha512-jEpglcwMlwdXc/JgvJaJtCSkPMktnFeI0gAZxPrmbJxKVzMZJ2zM582NbW/r6M22pSdNWjcWeg1I2LRg3jQGQA==" | |||
| }, | |||
| "@ngrx/effects": { | |||
| "version": "4.1.1", | |||
| "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-4.1.1.tgz", | |||
| "integrity": "sha1-y3WLhSeWSyWOpBlR9ZqhROPvn64=" | |||
| }, | |||
| "@ngrx/router-store": { | |||
| "version": "4.1.1", | |||
| "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-4.1.1.tgz", | |||
| "integrity": "sha1-F/rHwPX/3e+LdemnTtLLCQdPO8o=" | |||
| }, | |||
| "@ngrx/store": { | |||
| "version": "4.1.1", | |||
| "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-4.1.1.tgz", | |||
| "integrity": "sha1-aA403yd16IUnVO13f/rJW9gbfeA=" | |||
| }, | |||
| "@ngrx/store-devtools": { | |||
| "version": "4.1.1", | |||
| "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-4.1.1.tgz", | |||
| "integrity": "sha1-IHRcOcdWD9wF+k8iY4RCp+x91nY=" | |||
| }, | |||
| "@ngtools/json-schema": { | |||
| "version": "1.1.0", | |||
| "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", | |||
| @@ -21,6 +21,10 @@ | |||
| "@angular/platform-browser": "^5.0.0", | |||
| "@angular/platform-browser-dynamic": "^5.0.0", | |||
| "@angular/router": "^5.0.0", | |||
| "@ngrx/effects": "^4.1.1", | |||
| "@ngrx/router-store": "^4.1.1", | |||
| "@ngrx/store": "^4.1.1", | |||
| "@ngrx/store-devtools": "^4.1.1", | |||
| "bootstrap": "^3.3.7", | |||
| "core-js": "^2.4.1", | |||
| "firebase": "^4.6.2", | |||
| @@ -1,6 +1,11 @@ | |||
| import { BrowserModule } from '@angular/platform-browser'; | |||
| import { NgModule } from '@angular/core'; | |||
| import { HttpClientModule } from '@angular/common/http'; | |||
| import { StoreModule } from '@ngrx/store'; | |||
| import { EffectsModule } from '@ngrx/effects'; | |||
| import { StoreRouterConnectingModule } from '@ngrx/router-store'; | |||
| import { StoreDevtoolsModule } from '@ngrx/store-devtools'; | |||
| import { environment } from '../environments/environment'; | |||
| import { AppComponent } from './app.component'; | |||
| import { AppRoutingModule } from './app-routing.module'; | |||
| @@ -8,6 +13,8 @@ import { SharedModule } from './shared/shared.module'; | |||
| import { ShoppingListModule } from './shopping-list/shopping-list.module'; | |||
| import { AuthModule } from './auth/auth.module'; | |||
| import { CoreModule } from './core/core.module'; | |||
| import { appReducers } from './ngrx/app.reducers'; | |||
| import { AuthEffects } from './auth/ngrx/auth.effects'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| @@ -20,7 +27,11 @@ import { CoreModule } from './core/core.module'; | |||
| SharedModule, | |||
| ShoppingListModule, | |||
| AuthModule, | |||
| CoreModule | |||
| CoreModule, | |||
| StoreModule.forRoot(appReducers), | |||
| EffectsModule.forRoot([AuthEffects]), | |||
| StoreRouterConnectingModule, | |||
| !environment.production ? StoreDevtoolsModule.instrument() : [] | |||
| ], | |||
| bootstrap: [AppComponent] | |||
| }) | |||
| @@ -1,14 +1,22 @@ | |||
| import { CanActivate } from "@angular/router"; | |||
| import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router/src/router_state"; | |||
| import { Injectable } from "@angular/core"; | |||
| import { AuthService } from "./auth.service"; | |||
| import { Store } from "@ngrx/store"; | |||
| import "rxjs/add/operator/map"; | |||
| import * as fromApp from '../ngrx/app.reducers'; | |||
| import * as fromAuth from './ngrx/auth.reducers'; | |||
| @Injectable() | |||
| export class AuthGuard implements CanActivate { | |||
| constructor(private authService: AuthService) {} | |||
| constructor(private store: Store<fromApp.AppState>) {} | |||
| canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | |||
| return this.authService.isAuthenticated(); | |||
| return this.store.select('auth') | |||
| .take(1) | |||
| .map((authState: fromAuth.State) => { | |||
| return authState.authenticated; | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,50 +0,0 @@ | |||
| import * as firebase from 'firebase'; | |||
| import { Router } from '@angular/router'; | |||
| import { Injectable } from '@angular/core'; | |||
| @Injectable() | |||
| export class AuthService { | |||
| token: string; | |||
| constructor(private router: Router) {} | |||
| signupUser(email: string, password: string) { | |||
| firebase.auth().createUserWithEmailAndPassword(email, password).catch( | |||
| error => console.log(error) | |||
| ); | |||
| } | |||
| signinUser(email: string, password: string) { | |||
| firebase.auth().signInWithEmailAndPassword(email, password) | |||
| .then( | |||
| response => { | |||
| this.router.navigate(['/']); | |||
| firebase.auth().currentUser.getIdToken() | |||
| .then( | |||
| (token: string) => this.token = token | |||
| ); | |||
| } | |||
| ) | |||
| .catch( | |||
| error => console.log(error) | |||
| ); | |||
| } | |||
| logout() { | |||
| firebase.auth().signOut(); | |||
| this.token = null; | |||
| this.router.navigate(['/']); | |||
| } | |||
| getToken() { | |||
| firebase.auth().currentUser.getIdToken() | |||
| .then( | |||
| (token: string) => this.token = token | |||
| ); | |||
| return this.token; | |||
| } | |||
| isAuthenticated() { | |||
| return this.token != null; | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| import { Action } from '@ngrx/store'; | |||
| export const TRY_SIGNUP = 'TRY_SIGNUP'; | |||
| export const SIGNUP = 'SIGNUP'; | |||
| export const SIGNIN = 'SIGNIN'; | |||
| export const TRY_SIGNIN = 'TRY_SIGNIN'; | |||
| export const LOGOUT = 'LOGOUT'; | |||
| export const SET_TOKEN = 'SET_TOKEN'; | |||
| export class TrySignup implements Action { | |||
| readonly type = TRY_SIGNUP; | |||
| constructor(public payload: {username: string, password: string}) {} | |||
| } | |||
| export class Signup implements Action { | |||
| readonly type = SIGNUP; | |||
| } | |||
| export class TrySignin implements Action { | |||
| readonly type = TRY_SIGNIN; | |||
| constructor(public payload: {username: string, password: string}) {} | |||
| } | |||
| export class Signin implements Action { | |||
| readonly type = SIGNIN; | |||
| } | |||
| export class Logout implements Action { | |||
| readonly type = LOGOUT; | |||
| } | |||
| export class SetToken implements Action { | |||
| readonly type = SET_TOKEN; | |||
| constructor(public payload: string) {} | |||
| } | |||
| export type AuthActions = TrySignup | Signup | TrySignin | Signin | Logout | SetToken; | |||
| @@ -0,0 +1,62 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { Router } from '@angular/router'; | |||
| import { Actions, Effect } from '@ngrx/effects'; | |||
| import 'rxjs/add/operator/map'; | |||
| import 'rxjs/add/operator/do'; | |||
| import 'rxjs/add/operator/switchMap'; | |||
| import 'rxjs/add/operator/mergeMap'; | |||
| import { fromPromise } from 'rxjs/observable/fromPromise'; | |||
| import * as firebase from 'firebase'; | |||
| import * as AuthActions from './auth.actions'; | |||
| import * as fromAuth from './auth.reducers'; | |||
| @Injectable() | |||
| export class AuthEffects { | |||
| @Effect() | |||
| authSignup = this.actions$.ofType(AuthActions.TRY_SIGNUP) | |||
| .map((action: AuthActions.TrySignup) => { | |||
| return action.payload; | |||
| }) | |||
| .switchMap((authData: {username: string, password: string}) => { | |||
| return fromPromise(firebase.auth().createUserWithEmailAndPassword(authData.username, authData.password)); | |||
| }) | |||
| .switchMap(() => { | |||
| return fromPromise(firebase.auth().currentUser.getIdToken()); | |||
| }) | |||
| .mergeMap((token: string) => { | |||
| this.router.navigate(['/']); | |||
| return [ | |||
| { type: AuthActions.SIGNUP }, | |||
| { type: AuthActions.SET_TOKEN, payload: token } | |||
| ]; | |||
| }); | |||
| @Effect() | |||
| authSignin = this.actions$.ofType(AuthActions.TRY_SIGNIN) | |||
| .map((action: AuthActions.TrySignup) => { | |||
| return action.payload; | |||
| }) | |||
| .switchMap((authData: {username: string, password: string}) => { | |||
| return fromPromise(firebase.auth().signInWithEmailAndPassword(authData.username, authData.password)); | |||
| }) | |||
| .switchMap(() => { | |||
| return fromPromise(firebase.auth().currentUser.getIdToken()); | |||
| }) | |||
| .mergeMap((token: string) => { | |||
| this.router.navigate(['/']); | |||
| return [ | |||
| { type: AuthActions.SIGNIN }, | |||
| { type: AuthActions.SET_TOKEN, payload: token } | |||
| ] | |||
| }); | |||
| @Effect({dispatch: false}) | |||
| authLogout = this.actions$.ofType(AuthActions.LOGOUT) | |||
| .do(() => { | |||
| this.router.navigate(['/']); | |||
| }); | |||
| // variable with dollar sign at the end marks an observable | |||
| constructor(private router: Router, private actions$: Actions) {} | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| import * as AuthActions from './auth.actions'; | |||
| export interface State { | |||
| token: string; | |||
| authenticated: boolean; | |||
| } | |||
| const initialState: State = { | |||
| token: null, | |||
| authenticated: false | |||
| }; | |||
| export function authReducers(state = initialState, action: AuthActions.AuthActions) { | |||
| switch (action.type) { | |||
| case AuthActions.SIGNUP: | |||
| case AuthActions.SIGNIN: | |||
| return { | |||
| ...state, | |||
| authenticated: true | |||
| }; | |||
| case AuthActions.LOGOUT: | |||
| return { | |||
| ...state, | |||
| token: null, | |||
| authenticated: false | |||
| }; | |||
| case AuthActions.SET_TOKEN: | |||
| return { | |||
| ...state, | |||
| token: action.payload | |||
| }; | |||
| default: | |||
| return state; | |||
| } | |||
| } | |||
| @@ -1,6 +1,9 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { NgForm } from '@angular/forms'; | |||
| import { AuthService } from '../auth.service'; | |||
| import { Store } from '@ngrx/store'; | |||
| import * as fromApp from '../../ngrx/app.reducers'; | |||
| import * as AuthActions from '../ngrx/auth.actions'; | |||
| @Component({ | |||
| selector: 'app-signin', | |||
| @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; | |||
| }) | |||
| export class SigninComponent implements OnInit { | |||
| constructor(private authService: AuthService) { } | |||
| constructor(private store: Store<fromApp.AppState>) { } | |||
| ngOnInit() { | |||
| } | |||
| @@ -17,7 +20,7 @@ export class SigninComponent implements OnInit { | |||
| onSignin(form: NgForm) { | |||
| const email = form.value.email; | |||
| const password = form.value.password; | |||
| this.authService.signinUser(email, password); | |||
| this.store.dispatch(new AuthActions.TrySignin({username: email, password: password})); | |||
| } | |||
| } | |||
| @@ -1,6 +1,9 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { NgForm } from '@angular/forms'; | |||
| import { AuthService } from '../auth.service'; | |||
| import { Store } from '@ngrx/store'; | |||
| import * as fromApp from '../../ngrx/app.reducers'; | |||
| import * as AuthActions from '../ngrx/auth.actions'; | |||
| @Component({ | |||
| selector: 'app-signup', | |||
| @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; | |||
| }) | |||
| export class SignupComponent implements OnInit { | |||
| constructor(private authService: AuthService) { } | |||
| constructor(private store: Store<fromApp.AppState>) { } | |||
| ngOnInit() { | |||
| } | |||
| @@ -17,7 +20,7 @@ export class SignupComponent implements OnInit { | |||
| onSignup(form: NgForm) { | |||
| const email = form.value.email; | |||
| const password = form.value.password; | |||
| this.authService.signupUser(email, password); | |||
| this.store.dispatch(new AuthActions.TrySignup({username: email, password: password})); | |||
| } | |||
| } | |||
| @@ -5,10 +5,6 @@ import { HeaderComponent } from "./header/header.component"; | |||
| import { HomeComponent } from "./home/home.component"; | |||
| import { SharedModule } from "../shared/shared.module"; | |||
| import { AppRoutingModule } from "../app-routing.module"; | |||
| import { ShoppingListService } from "../shopping-list/shopping-list.service"; | |||
| import { RecipeService } from "../recipes/recipe.service"; | |||
| import { DataStorageService } from "../shared/data-storage.service"; | |||
| import { AuthService } from "../auth/auth.service"; | |||
| import { AuthInterceptor } from "../shared/auth.interceptor"; | |||
| import { LoggingInterceptor } from "../shared/logging.interceptor"; | |||
| @@ -26,10 +22,6 @@ import { LoggingInterceptor } from "../shared/logging.interceptor"; | |||
| HeaderComponent | |||
| ], | |||
| providers: [ | |||
| ShoppingListService, | |||
| RecipeService, | |||
| DataStorageService, | |||
| AuthService, | |||
| {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, | |||
| {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true} | |||
| ], | |||
| @@ -10,12 +10,12 @@ | |||
| <li routerLinkActive="active"><a routerLink="/shopping-list">Shopping List</a></li> | |||
| </ul> | |||
| <ul class="nav navbar-nav navbar-right"> | |||
| <ng-template [ngIf]="!isAuthenticated()"> | |||
| <ng-template [ngIf]="!(authState | async).authenticated"> | |||
| <li><a routerLink="/signup">Register</a></li> | |||
| <li><a routerLink="/signin">Login</a></li> | |||
| </ng-template> | |||
| <li><a style="cursor: pointer;" (click)="onLogout()" *ngIf="isAuthenticated()">Logout</a></li> | |||
| <li class="dropdown" appDropdown *ngIf="isAuthenticated()"> | |||
| <li><a style="cursor: pointer;" (click)="onLogout()" *ngIf="(authState | async).authenticated">Logout</a></li> | |||
| <li class="dropdown" appDropdown *ngIf="(authState | async).authenticated"> | |||
| <a style="cursor: pointer;" class="dropdown-toggle" role="button">Manage <span class="caret"></span></a> | |||
| <ul class="dropdown-menu"> | |||
| <li><a style="cursor: pointer;" (click)="onSaveData()">Save</a></li> | |||
| @@ -1,32 +1,34 @@ | |||
| import { Component } from "@angular/core"; | |||
| import { DataStorageService } from "../../shared/data-storage.service"; | |||
| import { AuthService } from "../../auth/auth.service"; | |||
| import { Component, OnInit } from "@angular/core"; | |||
| import { Store } from "@ngrx/store"; | |||
| import { Observable } from "rxjs/Observable"; | |||
| 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', | |||
| templateUrl: './header.component.html' | |||
| }) | |||
| export class HeaderComponent { | |||
| export class HeaderComponent implements OnInit { | |||
| authState: Observable<fromAuth.State>; | |||
| constructor(private store: Store<fromApp.AppState>) {} | |||
| constructor(private dataStorageService: DataStorageService, private authService: AuthService) {} | |||
| ngOnInit() { | |||
| this.authState = this.store.select('auth') | |||
| } | |||
| onSaveData() { | |||
| this.dataStorageService.storeRecipes().subscribe( | |||
| response => { | |||
| // console.log(response); | |||
| } | |||
| ); | |||
| this.store.dispatch(new RecipeActions.StoreRecipes()); | |||
| } | |||
| onFetchData() { | |||
| this.dataStorageService.fetchRecipes(); | |||
| this.store.dispatch(new RecipeActions.FetchRecipes()); | |||
| } | |||
| onLogout() { | |||
| this.authService.logout(); | |||
| } | |||
| isAuthenticated() { | |||
| return this.authService.isAuthenticated(); | |||
| this.store.dispatch(new AuthActions.Logout()); | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| import { ActionReducerMap } from '@ngrx/store/src/models'; | |||
| import * as fromShoppingList from '../shopping-list/ngrx/shopping-list.reducers' | |||
| import * as fromAuth from '../auth/ngrx/auth.reducers' | |||
| export interface AppState { | |||
| shoppingList: fromShoppingList.State, | |||
| auth: fromAuth.State | |||
| } | |||
| export const appReducers: ActionReducerMap<AppState> = { | |||
| shoppingList: fromShoppingList.shoppingListReducer, | |||
| auth: fromAuth.authReducers | |||
| }; | |||
| @@ -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,39 @@ | |||
| import { Injectable } from "@angular/core"; | |||
| import { Actions, Effect } from "@ngrx/effects"; | |||
| import 'rxjs/add/operator/map'; | |||
| import 'rxjs/add/operator/switchMap'; | |||
| import 'rxjs/add/operator/withLatestFrom'; | |||
| import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'; | |||
| import { Store } from "@ngrx/store"; | |||
| import * as RecipeActions from '../ngrx/recipe.actions'; | |||
| import { Recipe } from "../recipe.model"; | |||
| import * as fromRecipe from './recipe.reducers'; | |||
| @Injectable() | |||
| export class RecipeEffects { | |||
| readonly baseUrl: string = 'https://my-recipe-book-cb837.firebaseio.com/'; | |||
| constructor(private actions$: Actions, | |||
| private httpClient: HttpClient, | |||
| private store: Store<fromRecipe.FeatureState>) {} | |||
| @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 | |||
| }; | |||
| }); | |||
| @Effect({dispatch: false}) | |||
| recipeStore = this.actions$.ofType(RecipeActions.STORE_RECIPES) | |||
| .withLatestFrom(this.store.select('recipes')) | |||
| .switchMap(([action, state]) => { | |||
| return this.httpClient.put(this.baseUrl + 'recipes.json', state.recipes); | |||
| }); | |||
| } | |||
| @@ -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: [] | |||
| }; | |||
| 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,7 +34,7 @@ | |||
| <ul class="list-group"> | |||
| <li | |||
| class="list-group-item" | |||
| *ngFor="let ingredient of recipe.ingredients"> | |||
| *ngFor="let ingredient of (recipeState | async).recipes[id].ingredients"> | |||
| {{ ingredient.name }} - {{ ingredient.amount }} | |||
| </li> | |||
| </ul> | |||
| @@ -1,8 +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', | |||
| @@ -10,24 +15,29 @@ import { RecipeService } from '../recipe.service'; | |||
| 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, | |||
| private router: Router) { } | |||
| constructor(private route: ActivatedRoute, | |||
| private router: Router, | |||
| 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.recipeService.addIngredientsToShoppingList(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() { | |||
| @@ -35,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,27 @@ | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -1,46 +0,0 @@ | |||
| import { Recipe } from "./recipe.model"; | |||
| import { EventEmitter, Injectable } from "@angular/core"; | |||
| import { Ingredient } from "../shared/ingredient.model"; | |||
| import { ShoppingListService } from "../shopping-list/shopping-list.service"; | |||
| import { Subject } from "rxjs/Subject"; | |||
| import { nextTick } from "q"; | |||
| @Injectable() | |||
| export class RecipeService { | |||
| recipesChanged = new Subject<Recipe[]>(); | |||
| private recipes: Recipe[] = []; | |||
| constructor(private shoppingListService: ShoppingListService) { } | |||
| replaceRecipes(recipes: Recipe[]) { | |||
| this.recipes = recipes; | |||
| this.recipesChanged.next(this.recipes.slice()); | |||
| } | |||
| getRecipes() { | |||
| return this.recipes.slice(); | |||
| } | |||
| getRecipe(index: number) { | |||
| return this.recipes[index]; | |||
| } | |||
| addIngredientsToShoppingList(ingredients: Ingredient[]) { | |||
| this.shoppingListService.addIngredients(ingredients); | |||
| } | |||
| 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,7 +1,5 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { RecipeService } from './recipe.service'; | |||
| @Component({ | |||
| selector: 'app-recipes', | |||
| templateUrl: './recipes.component.html', | |||
| @@ -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,16 +1,23 @@ | |||
| import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http' | |||
| import { Observable } from 'rxjs/Observable'; | |||
| import { Injectable } from '@angular/core'; | |||
| import { AuthService } from '../auth/auth.service'; | |||
| import { Store } from "@ngrx/store"; | |||
| import "rxjs/add/operator/switchMap"; | |||
| import "rxjs/add/operator/take"; | |||
| import * as fromApp from '../ngrx/app.reducers'; | |||
| import * as fromAuth from '../auth/ngrx/auth.reducers'; | |||
| @Injectable() | |||
| export class AuthInterceptor implements HttpInterceptor { | |||
| constructor(private authService: AuthService) {} | |||
| constructor(private store: Store<fromApp.AppState>) {} | |||
| intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||
| const copiedReq = req.clone({ | |||
| params: req.params.set('auth', this.authService.getToken()) | |||
| }); | |||
| return next.handle(copiedReq); | |||
| return this.store.select('auth') | |||
| .take(1) | |||
| .switchMap((authState: fromAuth.State) => { | |||
| const copiedReq = req.clone({params: req.params.set('auth', authState.token)}); | |||
| return next.handle(copiedReq); | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,29 +0,0 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | |||
| import { RecipeService } from '../recipes/recipe.service'; | |||
| import { Recipe } from '../recipes/recipe.model'; | |||
| @Injectable() | |||
| export class DataStorageService { | |||
| readonly baseUrl: string = 'https://my-recipe-book-cb837.firebaseio.com/'; | |||
| constructor(private httpClient: HttpClient, | |||
| private recipeService: RecipeService) {} | |||
| storeRecipes() { | |||
| // const token = this.authService.getToken(); | |||
| // return this.httpClient.put(this.baseUrl + 'recipes.json?auth=' + token, this.recipeService.getRecipes()); | |||
| // const req = new HttpRequest('PUT', this.baseUrl, this.recipeService.getRecipes(), {reportProgress: true, params: new HttpParams().set('auth', token)}); | |||
| // return this.httpClient.request(req); | |||
| return this.httpClient.put(this.baseUrl + 'recipes.json', this.recipeService.getRecipes()); | |||
| } | |||
| fetchRecipes() { | |||
| this.httpClient.get<Recipe[]>(this.baseUrl + 'recipes.json').subscribe( | |||
| recipes => { | |||
| this.recipeService.replaceRecipes(recipes); | |||
| } | |||
| ) | |||
| } | |||
| } | |||
| @@ -4,10 +4,10 @@ import 'rxjs/add/operator/do'; | |||
| export class LoggingInterceptor implements HttpInterceptor { | |||
| intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||
| // do without consuming it! | |||
| // do() without consuming it! | |||
| return next.handle(req).do( | |||
| event => { | |||
| console.log('Logging interceptor', event); | |||
| // console.log('Logging interceptor', event); | |||
| } | |||
| ) | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| import { Action } from '@ngrx/store'; | |||
| import { Ingredient } from '../../shared/ingredient.model'; | |||
| export const ADD_INGREDIENT = 'ADD_INGREDIENT'; | |||
| export const ADD_INGREDIENTS = 'ADD_INGREDIENTS'; | |||
| export const UPDATE_INGREDIENT = 'UPDATE_INGREDIENT'; | |||
| export const DELETE_INGREDIENT = 'DELETE_INGREDIENT'; | |||
| export const START_EDIT = 'START_EDIT'; | |||
| export const STOP_EDIT = 'STOP_EDIT'; | |||
| export class AddIngredient implements Action { | |||
| readonly type = ADD_INGREDIENT; | |||
| constructor(public payload: Ingredient) {} | |||
| } | |||
| export class AddIngredients implements Action { | |||
| readonly type = ADD_INGREDIENTS; | |||
| constructor(public payload: Ingredient[]) {} | |||
| } | |||
| export class UpdateIngredient implements Action { | |||
| readonly type = UPDATE_INGREDIENT; | |||
| constructor(public payload: Ingredient) {} | |||
| } | |||
| export class DeleteIngredient implements Action { | |||
| readonly type = DELETE_INGREDIENT; | |||
| } | |||
| export class StartEdit implements Action { | |||
| readonly type = START_EDIT; | |||
| constructor(public payload: number) {} | |||
| } | |||
| export class StopEdit implements Action { | |||
| readonly type = STOP_EDIT; | |||
| } | |||
| export type ShoppingListActions = AddIngredient | AddIngredients | UpdateIngredient | DeleteIngredient | StartEdit | StopEdit; | |||
| @@ -0,0 +1,70 @@ | |||
| import * as ShoppingListActions from './shopping-list.actions'; | |||
| import { Ingredient } from '../../shared/ingredient.model'; | |||
| export interface State { | |||
| ingredients: Ingredient[]; | |||
| editedIngredient: Ingredient; | |||
| editedIngredientIndex: number; | |||
| } | |||
| const initialState: State = { | |||
| ingredients: [ | |||
| new Ingredient('Banana', 10) | |||
| ], | |||
| editedIngredient: null, | |||
| editedIngredientIndex: -1 | |||
| }; | |||
| export function shoppingListReducer(state = initialState, action: ShoppingListActions.ShoppingListActions) { | |||
| switch (action.type) { | |||
| case ShoppingListActions.ADD_INGREDIENT: | |||
| return { | |||
| ...state, | |||
| ingredients: [...state.ingredients, action.payload] | |||
| }; | |||
| case ShoppingListActions.ADD_INGREDIENTS: | |||
| return { | |||
| ...state, | |||
| ingredients: [...state.ingredients, ...action.payload] | |||
| }; | |||
| case ShoppingListActions.UPDATE_INGREDIENT: | |||
| const ingredient = state.ingredients[state.editedIngredientIndex]; | |||
| const updatedIngredient = { | |||
| ...ingredient, | |||
| ...action.payload | |||
| } | |||
| const ingredients = [...state.ingredients]; | |||
| ingredients[state.editedIngredientIndex] = updatedIngredient; | |||
| return { | |||
| ...state, | |||
| ingredients: ingredients, | |||
| editedIngredient: null, | |||
| editedIngredientIndex: -1 | |||
| }; | |||
| case ShoppingListActions.DELETE_INGREDIENT: | |||
| const oldIngredients = [...state.ingredients]; | |||
| oldIngredients.splice(state.editedIngredientIndex, 1); | |||
| return { | |||
| ...state, | |||
| ingredients: oldIngredients, | |||
| editedIngredient: null, | |||
| editedIngredientIndex: -1 | |||
| }; | |||
| case ShoppingListActions.START_EDIT: | |||
| const editedIngredient = {...state.ingredients[action.payload]}; | |||
| return { | |||
| ...state, | |||
| editedIngredient: editedIngredient, | |||
| editedIngredientIndex: action.payload | |||
| } | |||
| case ShoppingListActions.STOP_EDIT: | |||
| return { | |||
| ...state, | |||
| editedIngredient: null, | |||
| editedIngredientIndex: -1 | |||
| } | |||
| default: | |||
| return state; | |||
| } | |||
| } | |||
| @@ -1,9 +1,11 @@ | |||
| import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; | |||
| import { NgForm } from '@angular/forms'; | |||
| import { Subscription } from 'rxjs/Subscription'; | |||
| import { Store } from '@ngrx/store'; | |||
| import { Ingredient } from '../../shared/ingredient.model'; | |||
| import { ShoppingListService } from '../shopping-list.service'; | |||
| import * as ShoppingListActions from '../ngrx/shopping-list.actions'; | |||
| import * as fromApp from '../../ngrx/app.reducers' | |||
| @Component({ | |||
| selector: 'app-shopping-edit', | |||
| @@ -14,21 +16,23 @@ export class ShoppingEditComponent implements OnInit, OnDestroy { | |||
| @ViewChild('f') shoppingListForm: NgForm; | |||
| subscription: Subscription | |||
| editMode = false; | |||
| editedItemIndex: number; | |||
| editedItem: Ingredient; | |||
| constructor(private shoppingListService: ShoppingListService) { } | |||
| constructor(private store: Store<fromApp.AppState>) { } | |||
| ngOnInit() { | |||
| this.subscription = this.shoppingListService.startedEditing.subscribe( | |||
| (index: number) => { | |||
| this.editedItemIndex = index; | |||
| this.editMode = true; | |||
| this.editedItem = this.shoppingListService.getIngredient(index); | |||
| this.shoppingListForm.setValue({ | |||
| name: this.editedItem.name, | |||
| amount: this.editedItem.amount | |||
| }) | |||
| this.subscription = this.store.select('shoppingList').subscribe( | |||
| data => { | |||
| if (data.editedIngredientIndex > -1) { | |||
| this.editedItem = data.editedIngredient; | |||
| this.editMode = true; | |||
| this.shoppingListForm.setValue({ | |||
| name: this.editedItem.name, | |||
| amount: this.editedItem.amount | |||
| }) | |||
| } else { | |||
| this.editMode = false; | |||
| } | |||
| } | |||
| ); | |||
| } | |||
| @@ -37,9 +41,9 @@ export class ShoppingEditComponent implements OnInit, OnDestroy { | |||
| const value = form.value | |||
| const newIngredient = new Ingredient(value.name, value.amount); | |||
| if (this.editMode) { | |||
| this.shoppingListService.updateIngredient(this.editedItemIndex, newIngredient); | |||
| this.store.dispatch(new ShoppingListActions.UpdateIngredient(newIngredient)); | |||
| } else { | |||
| this.shoppingListService.addIngredient(newIngredient); | |||
| this.store.dispatch(new ShoppingListActions.AddIngredient(newIngredient)); | |||
| } | |||
| this.editMode = false; | |||
| form.reset(); | |||
| @@ -51,11 +55,12 @@ export class ShoppingEditComponent implements OnInit, OnDestroy { | |||
| } | |||
| onDelete() { | |||
| this.shoppingListService.deleteIngredient(this.editedItemIndex); | |||
| this.store.dispatch(new ShoppingListActions.DeleteIngredient()); | |||
| this.onClear(); | |||
| } | |||
| ngOnDestroy() { | |||
| this.store.dispatch(new ShoppingListActions.StopEdit()); | |||
| this.subscription.unsubscribe(); | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ | |||
| <a | |||
| class="list-group-item" | |||
| style="cursor: pointer;" | |||
| *ngFor="let ingredient of ingredients; let i = index" | |||
| *ngFor="let ingredient of (shoppingListState | async).ingredients; let i = index" | |||
| (click)="onEditItem(i)" | |||
| > | |||
| {{ ingredient.name }} ({{ ingredient.amount }}) | |||
| @@ -1,34 +1,26 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { Store } from '@ngrx/store'; | |||
| import { Observable } from 'rxjs/Observable'; | |||
| import { Ingredient } from '../shared/ingredient.model'; | |||
| import { ShoppingListService } from './shopping-list.service'; | |||
| import { Subscription } from 'rxjs/Subscription'; | |||
| import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks'; | |||
| import * as ShoppingListActions from './ngrx/shopping-list.actions'; | |||
| import * as fromApp from '../ngrx/app.reducers' | |||
| @Component({ | |||
| selector: 'app-shopping-list', | |||
| templateUrl: './shopping-list.component.html', | |||
| styleUrls: ['./shopping-list.component.css'] | |||
| }) | |||
| export class ShoppingListComponent implements OnInit, OnDestroy { | |||
| ingredients: Ingredient[]; | |||
| private subscription: Subscription | |||
| export class ShoppingListComponent implements OnInit { | |||
| shoppingListState: Observable<{ingredients: Ingredient[]}>; | |||
| constructor(private shoppingListService: ShoppingListService) { } | |||
| constructor(private store: Store<fromApp.AppState>) { } | |||
| ngOnInit() { | |||
| this.ingredients = this.shoppingListService.getIngredients(); | |||
| this.subscription = this.shoppingListService.ingredientsChanged.subscribe( | |||
| (ingredients: Ingredient[]) => { | |||
| this.ingredients = ingredients; | |||
| } | |||
| ); | |||
| this.shoppingListState = this.store.select('shoppingList'); | |||
| } | |||
| onEditItem(index: number) { | |||
| this.shoppingListService.startedEditing.next(index); | |||
| } | |||
| ngOnDestroy() { | |||
| this.subscription.unsubscribe(); | |||
| this.store.dispatch(new ShoppingListActions.StartEdit(index)); | |||
| } | |||
| } | |||
| @@ -1,36 +0,0 @@ | |||
| import { Ingredient } from "../shared/ingredient.model"; | |||
| import { Subject } from "rxjs/Subject"; | |||
| export class ShoppingListService { | |||
| ingredientsChanged = new Subject<Ingredient[]>(); | |||
| startedEditing = new Subject<number>(); | |||
| private ingredients: Ingredient[] = []; | |||
| getIngredients() { | |||
| return this.ingredients.slice(); | |||
| } | |||
| getIngredient(index: number) { | |||
| return this.ingredients[index]; | |||
| } | |||
| addIngredient(ingredient: Ingredient) { | |||
| this.ingredients.push(ingredient); | |||
| this.ingredientsChanged.next(this.ingredients.slice()); | |||
| } | |||
| addIngredients(ingredients: Ingredient[]) { | |||
| this.ingredients.push(...ingredients); | |||
| this.ingredientsChanged.next(this.ingredients.slice()); | |||
| } | |||
| updateIngredient(index: number, newIngredient: Ingredient) { | |||
| this.ingredients[index] = newIngredient; | |||
| this.ingredientsChanged.next(this.ingredients.slice()); | |||
| } | |||
| deleteIngredient(index: number) { | |||
| this.ingredients.splice(index, 1); | |||
| this.ingredientsChanged.next(this.ingredients.slice()); | |||
| } | |||
| } | |||