| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
e9aba1d7f3 | Merge branch 'ngrx' | 8 years ago |
|
|
e3a46d8895 | 24-343 Cleaning up | 8 years ago |
|
|
66bb853828 | 24-342 Storing recipes via ngrx | 8 years ago |
|
|
1bdf8d4131 | 24-341 Fetching data with ngrx | 8 years ago |
|
|
ddc7f7c3dc | 24-334 Adding store router and store devtools | 8 years ago |
|
|
ee038d91b5 | 24-332 Fixing something, removing auth service | 8 years ago |
|
|
415c979c98 | 24-328 Using ngrx for signin | 8 years ago |
|
|
e24fe62f50 | 24-327 Adding rxng effects | 8 years ago |
|
|
bae3653b7a | 24-322 Switching to NgRx for authentication | 8 years ago |
|
|
569a9da503 | 24-314 Moving data logic to NgRx, removing shopping list service | 8 years ago |
|
|
7aa097ca07 | 24-307 Setting state with actions | 8 years ago |
|
|
7e16021a74 | 24-307 Loading states with NgRx | 8 years ago |
| @@ -336,6 +336,26 @@ | |||||
| "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.4.tgz", | "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.4.tgz", | ||||
| "integrity": "sha512-jEpglcwMlwdXc/JgvJaJtCSkPMktnFeI0gAZxPrmbJxKVzMZJ2zM582NbW/r6M22pSdNWjcWeg1I2LRg3jQGQA==" | "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": { | "@ngtools/json-schema": { | ||||
| "version": "1.1.0", | "version": "1.1.0", | ||||
| "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", | "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": "^5.0.0", | ||||
| "@angular/platform-browser-dynamic": "^5.0.0", | "@angular/platform-browser-dynamic": "^5.0.0", | ||||
| "@angular/router": "^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", | "bootstrap": "^3.3.7", | ||||
| "core-js": "^2.4.1", | "core-js": "^2.4.1", | ||||
| "firebase": "^4.6.2", | "firebase": "^4.6.2", | ||||
| @@ -1,6 +1,11 @@ | |||||
| import { BrowserModule } from '@angular/platform-browser'; | import { BrowserModule } from '@angular/platform-browser'; | ||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||
| import { HttpClientModule } from '@angular/common/http'; | 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 { AppComponent } from './app.component'; | ||||
| import { AppRoutingModule } from './app-routing.module'; | 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 { ShoppingListModule } from './shopping-list/shopping-list.module'; | ||||
| import { AuthModule } from './auth/auth.module'; | import { AuthModule } from './auth/auth.module'; | ||||
| import { CoreModule } from './core/core.module'; | import { CoreModule } from './core/core.module'; | ||||
| import { appReducers } from './ngrx/app.reducers'; | |||||
| import { AuthEffects } from './auth/ngrx/auth.effects'; | |||||
| @NgModule({ | @NgModule({ | ||||
| declarations: [ | declarations: [ | ||||
| @@ -20,7 +27,11 @@ import { CoreModule } from './core/core.module'; | |||||
| SharedModule, | SharedModule, | ||||
| ShoppingListModule, | ShoppingListModule, | ||||
| AuthModule, | AuthModule, | ||||
| CoreModule | |||||
| CoreModule, | |||||
| StoreModule.forRoot(appReducers), | |||||
| EffectsModule.forRoot([AuthEffects]), | |||||
| StoreRouterConnectingModule, | |||||
| !environment.production ? StoreDevtoolsModule.instrument() : [] | |||||
| ], | ], | ||||
| bootstrap: [AppComponent] | bootstrap: [AppComponent] | ||||
| }) | }) | ||||
| @@ -1,14 +1,22 @@ | |||||
| import { CanActivate } from "@angular/router"; | import { CanActivate } from "@angular/router"; | ||||
| import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router/src/router_state"; | import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router/src/router_state"; | ||||
| import { Injectable } from "@angular/core"; | 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() | @Injectable() | ||||
| export class AuthGuard implements CanActivate { | export class AuthGuard implements CanActivate { | ||||
| constructor(private authService: AuthService) {} | |||||
| constructor(private store: Store<fromApp.AppState>) {} | |||||
| canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | 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 { Component, OnInit } from '@angular/core'; | ||||
| import { NgForm } from '@angular/forms'; | 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({ | @Component({ | ||||
| selector: 'app-signin', | selector: 'app-signin', | ||||
| @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; | |||||
| }) | }) | ||||
| export class SigninComponent implements OnInit { | export class SigninComponent implements OnInit { | ||||
| constructor(private authService: AuthService) { } | |||||
| constructor(private store: Store<fromApp.AppState>) { } | |||||
| ngOnInit() { | ngOnInit() { | ||||
| } | } | ||||
| @@ -17,7 +20,7 @@ export class SigninComponent implements OnInit { | |||||
| onSignin(form: NgForm) { | onSignin(form: NgForm) { | ||||
| const email = form.value.email; | const email = form.value.email; | ||||
| const password = form.value.password; | 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 { Component, OnInit } from '@angular/core'; | ||||
| import { NgForm } from '@angular/forms'; | 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({ | @Component({ | ||||
| selector: 'app-signup', | selector: 'app-signup', | ||||
| @@ -9,7 +12,7 @@ import { AuthService } from '../auth.service'; | |||||
| }) | }) | ||||
| export class SignupComponent implements OnInit { | export class SignupComponent implements OnInit { | ||||
| constructor(private authService: AuthService) { } | |||||
| constructor(private store: Store<fromApp.AppState>) { } | |||||
| ngOnInit() { | ngOnInit() { | ||||
| } | } | ||||
| @@ -17,7 +20,7 @@ export class SignupComponent implements OnInit { | |||||
| onSignup(form: NgForm) { | onSignup(form: NgForm) { | ||||
| const email = form.value.email; | const email = form.value.email; | ||||
| const password = form.value.password; | 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 { HomeComponent } from "./home/home.component"; | ||||
| import { SharedModule } from "../shared/shared.module"; | import { SharedModule } from "../shared/shared.module"; | ||||
| import { AppRoutingModule } from "../app-routing.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 { AuthInterceptor } from "../shared/auth.interceptor"; | ||||
| import { LoggingInterceptor } from "../shared/logging.interceptor"; | import { LoggingInterceptor } from "../shared/logging.interceptor"; | ||||
| @@ -26,10 +22,6 @@ import { LoggingInterceptor } from "../shared/logging.interceptor"; | |||||
| HeaderComponent | HeaderComponent | ||||
| ], | ], | ||||
| providers: [ | providers: [ | ||||
| ShoppingListService, | |||||
| RecipeService, | |||||
| DataStorageService, | |||||
| AuthService, | |||||
| {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, | {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, | ||||
| {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true} | {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true} | ||||
| ], | ], | ||||
| @@ -10,12 +10,12 @@ | |||||
| <li routerLinkActive="active"><a routerLink="/shopping-list">Shopping List</a></li> | <li routerLinkActive="active"><a routerLink="/shopping-list">Shopping List</a></li> | ||||
| </ul> | </ul> | ||||
| <ul class="nav navbar-nav navbar-right"> | <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="/signup">Register</a></li> | ||||
| <li><a routerLink="/signin">Login</a></li> | <li><a routerLink="/signin">Login</a></li> | ||||
| </ng-template> | </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> | <a style="cursor: pointer;" class="dropdown-toggle" role="button">Manage <span class="caret"></span></a> | ||||
| <ul class="dropdown-menu"> | <ul class="dropdown-menu"> | ||||
| <li><a style="cursor: pointer;" (click)="onSaveData()">Save</a></li> | <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({ | @Component({ | ||||
| selector: 'app-header', | selector: 'app-header', | ||||
| templateUrl: './header.component.html' | 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() { | onSaveData() { | ||||
| this.dataStorageService.storeRecipes().subscribe( | |||||
| response => { | |||||
| // console.log(response); | |||||
| } | |||||
| ); | |||||
| this.store.dispatch(new RecipeActions.StoreRecipes()); | |||||
| } | } | ||||
| onFetchData() { | onFetchData() { | ||||
| this.dataStorageService.fetchRecipes(); | |||||
| this.store.dispatch(new RecipeActions.FetchRecipes()); | |||||
| } | } | ||||
| onLogout() { | 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="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,7 +34,7 @@ | |||||
| <ul class="list-group"> | <ul class="list-group"> | ||||
| <li | <li | ||||
| class="list-group-item" | class="list-group-item" | ||||
| *ngFor="let ingredient of recipe.ingredients"> | |||||
| *ngFor="let ingredient of (recipeState | async).recipes[id].ingredients"> | |||||
| {{ ingredient.name }} - {{ ingredient.amount }} | {{ ingredient.name }} - {{ ingredient.amount }} | ||||
| </li> | </li> | ||||
| </ul> | </ul> | ||||
| @@ -1,8 +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 { 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 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', | ||||
| @@ -10,24 +15,29 @@ import { RecipeService } from '../recipe.service'; | |||||
| 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, | |||||
| private router: Router) { } | |||||
| constructor(private route: ActivatedRoute, | |||||
| private router: Router, | |||||
| 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.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() { | onEditRecipe() { | ||||
| @@ -35,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,27 @@ | |||||
| 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 { 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(); | |||||
| } | |||||
| } | } | ||||
| @@ -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 { Component, OnInit } from '@angular/core'; | ||||
| import { RecipeService } from './recipe.service'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-recipes', | selector: 'app-recipes', | ||||
| templateUrl: './recipes.component.html', | templateUrl: './recipes.component.html', | ||||
| @@ -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,16 +1,23 @@ | |||||
| import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http' | import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http' | ||||
| import { Observable } from 'rxjs/Observable'; | import { Observable } from 'rxjs/Observable'; | ||||
| import { Injectable } from '@angular/core'; | 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() | @Injectable() | ||||
| export class AuthInterceptor implements HttpInterceptor { | export class AuthInterceptor implements HttpInterceptor { | ||||
| constructor(private authService: AuthService) {} | |||||
| constructor(private store: Store<fromApp.AppState>) {} | |||||
| intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | 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 { | export class LoggingInterceptor implements HttpInterceptor { | ||||
| intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | ||||
| // do without consuming it! | |||||
| // do() without consuming it! | |||||
| return next.handle(req).do( | return next.handle(req).do( | ||||
| event => { | 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 { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; | ||||
| import { NgForm } from '@angular/forms'; | import { NgForm } from '@angular/forms'; | ||||
| import { Subscription } from 'rxjs/Subscription'; | import { Subscription } from 'rxjs/Subscription'; | ||||
| import { Store } from '@ngrx/store'; | |||||
| import { Ingredient } from '../../shared/ingredient.model'; | 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({ | @Component({ | ||||
| selector: 'app-shopping-edit', | selector: 'app-shopping-edit', | ||||
| @@ -14,21 +16,23 @@ export class ShoppingEditComponent implements OnInit, OnDestroy { | |||||
| @ViewChild('f') shoppingListForm: NgForm; | @ViewChild('f') shoppingListForm: NgForm; | ||||
| subscription: Subscription | subscription: Subscription | ||||
| editMode = false; | editMode = false; | ||||
| editedItemIndex: number; | |||||
| editedItem: Ingredient; | editedItem: Ingredient; | ||||
| constructor(private shoppingListService: ShoppingListService) { } | |||||
| constructor(private store: Store<fromApp.AppState>) { } | |||||
| ngOnInit() { | 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 value = form.value | ||||
| const newIngredient = new Ingredient(value.name, value.amount); | const newIngredient = new Ingredient(value.name, value.amount); | ||||
| if (this.editMode) { | if (this.editMode) { | ||||
| this.shoppingListService.updateIngredient(this.editedItemIndex, newIngredient); | |||||
| this.store.dispatch(new ShoppingListActions.UpdateIngredient(newIngredient)); | |||||
| } else { | } else { | ||||
| this.shoppingListService.addIngredient(newIngredient); | |||||
| this.store.dispatch(new ShoppingListActions.AddIngredient(newIngredient)); | |||||
| } | } | ||||
| this.editMode = false; | this.editMode = false; | ||||
| form.reset(); | form.reset(); | ||||
| @@ -51,11 +55,12 @@ export class ShoppingEditComponent implements OnInit, OnDestroy { | |||||
| } | } | ||||
| onDelete() { | onDelete() { | ||||
| this.shoppingListService.deleteIngredient(this.editedItemIndex); | |||||
| this.store.dispatch(new ShoppingListActions.DeleteIngredient()); | |||||
| this.onClear(); | this.onClear(); | ||||
| } | } | ||||
| ngOnDestroy() { | ngOnDestroy() { | ||||
| this.store.dispatch(new ShoppingListActions.StopEdit()); | |||||
| this.subscription.unsubscribe(); | this.subscription.unsubscribe(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -6,7 +6,7 @@ | |||||
| <a | <a | ||||
| class="list-group-item" | class="list-group-item" | ||||
| style="cursor: pointer;" | style="cursor: pointer;" | ||||
| *ngFor="let ingredient of ingredients; let i = index" | |||||
| *ngFor="let ingredient of (shoppingListState | async).ingredients; let i = index" | |||||
| (click)="onEditItem(i)" | (click)="onEditItem(i)" | ||||
| > | > | ||||
| {{ ingredient.name }} ({{ ingredient.amount }}) | {{ ingredient.name }} ({{ ingredient.amount }}) | ||||
| @@ -1,34 +1,26 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||
| import { Store } from '@ngrx/store'; | |||||
| import { Observable } from 'rxjs/Observable'; | |||||
| import { Ingredient } from '../shared/ingredient.model'; | 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({ | @Component({ | ||||
| selector: 'app-shopping-list', | selector: 'app-shopping-list', | ||||
| templateUrl: './shopping-list.component.html', | templateUrl: './shopping-list.component.html', | ||||
| styleUrls: ['./shopping-list.component.css'] | 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() { | 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) { | 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()); | |||||
| } | |||||
| } | |||||