- Clone repository
https://github.com/anuragsinghbisht/Angular2_FrontEnd.git
- Dependencies :
node > 6.9.1
&@angular/cli
- Install dependencies:
npm install
- Start Server:
npm start
- Build project:
npm run build
- Generating Documentation:
npm run compodoc
Note - Backend for this project is another github repository.
Angular Backend!
- Home Page : Displays Title And Description.
- Navigation Panel : Icons for Home Page, Add Product & Categories Page
- Categories Page : Displays categories
- Category List Page : Display list of product related to particular category
- Category Details Page: Displayes details of selected product
- Add Product Page : Form to enter product details
- @angular/cli
- angular-material
- @ngrx/store, @ngrx/core, @ngrx/effects
app
├── core
| ├── layout
| | └── header ( header.component.[ts/html/css] )
| ├── services
| | └── web.service.ts
| ├── models
| | └── [category/exercise/product].model.ts
| └── core.module.ts
├── shared
| └── shared.module.ts
├── home
| ├── components
| | └── banner ( banner.component.[ts/html/css] )
| ├── actions
| | └── home.actions.ts
| ├── reducers
| | └── home.reducers.ts
| ├── effects
| | └── home.effects.ts
| ├── state
| | └── home.state.ts / selectors.ts
| ├── home.routes.ts
| └── home.module.ts
├── category
| ├── actions
| | └── [category/product-form/product-list/product].actions.ts
| ├── components
| | ├── category-list ( category-list.component.[ts/html/css] )
| | ├── product-details ( product-details.component.[ts/html/css] )
| | ├── product-form ( product-form.component.[ts/html/css] )
| | └── product-list ( product-list.component.[ts/html/css] )
| ├── reducers
| | └── [category/product-form/product-list/product].reducers.ts
| ├── effects
| | └── category.effects.ts
| ├── state
| | ├── selectors.ts
| | └── [category/product-form/product-list/product].state.ts
| ├── category.routes.ts
| └── category.module.ts
├── app.component[ts/html/css]
├── app.routes.ts
├── app.reducers.ts
├── app.state.ts
├── app.effects.ts
└── app.module.ts
Note : This tutorial is step-by-step guide to integrate angular project with @ngrx. Basic knowledge of angular2+ and @angular/cli is required to understand the code.
- Create Project using @angular/cli (v 1.2.1) & install project dependencies
ng new angular-frontend cd angular-frontend npm install
- Run
npm start
to deploy project atlocalhost:4200
. Go to the localhost in your browser to see the created project. - Add coommonly used
rxjs
operators inapp.module.ts
.// adding rx operators import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/finally'; import 'rxjs/add/observable/of';
- Install @angular/material & material-design-icons
npm i -S @angular/material @angular/cdk material-design-icons
- To show icons available in material-icons, we need to add material-icons.css in .angular-cli.json.
"styles": [ "../node_modules/material-design-icons/iconfont/material-icons.css", "styles.css" ]
- Import MaterialModule and BrowserAnimationsModule in app.module.ts and add both in the
imports
array.import { MaterialModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; ... imports: [ BrowserModule,MaterialModule, BrowserAnimationsModule] ... ]
- Import material prebuilt themes in the style.css
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
- Install @ngrx/core @ngrx/store @nrgx/effects @ngrx/store-devtools
npm i -S @ngrx/core @ngrx/store @ngrx/effects @ngrx/store-devtools ngrx-store-freeze
- Import
ngrx modules
in app.module.tsimport { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { EffectsModule } from '@ngrx/effects';
- Add
modules
to theAppModule
( reducers/metaReducers/AppEffects to be added later)imports: [ StoreModule.forRoot({}), StoreDevtoolsModule.instrument(), EffectsModule.forRoot([]), ]
- Create shared module
ng g m shared
using @angular/cli - Add common modules to be shared across application.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MaterialModule } from '@angular/material'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [FormsModule, ReactiveFormsModule, CommonModule, MaterialModule], declarations: [], exports: [FormsModule, ReactiveFormsModule, CommonModule, MaterialModule] }) export class SharedModule {}
- Add SharedModule and remove MaterialModule from app.module.ts
import { SharedModule } from './shared/shared.module'; ... imports: [SharedModule] ...
- Create core module
ng g m core
using @angular/cli. Update core.module.ts to contain SharedModule which contains *MaterialModule.import { SharedModule } from '../shared/shared.module'
- Create layout folder to contain common components that will be used only one in the application. Let's create header component, which will contian 3 navigation items - home/add-product/category-list
ng g c core/layout/header
- This will generate 3 files in core/layout/header folder
- header.component[ts/html/css]
- In header.component.ts, selector field contains the name app-header by which we can use this component in other html files
- In core.module.js, angular-cli will automatically add the HeaderComponent in the file and the declarations.
- In core.module.js, add HeaderComponent in the exports variable also, as we will use it in other modules.
exports: [HeaderComponent]
- Add header in header.component.html.
<md-toolbar color="primary"> <span>Angular JS</span> <span class="fill-space"></span> <button md-icon-button><md-icon>home</md-icon></button> <button md-icon-button><md-icon>add</md-icon></button> <button md-icon-button><md-icon>dashboard</md-icon></button> </md-toolbar>
- Add app-header in the app.component.html
<app-header></app-header>
- Create web.service.ts to retrieve data from backend
ng g m core/services/web
-
import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; @Injectable() export class WebService { BASE_URL = 'https://angular-backend.herokuapp.com/api'; constructor(private http: Http) {} getTitleAndDescription() { return this.http.get(this.BASE_URL).map(res => res.json()); } getCategories() { return this.http.get(`${this.BASE_URL}/category`).map(res => res.json()); } getProducts(category) { return this.http .get(`${this.BASE_URL}/${category}/product`) .map(res => res.json()); } getProduct({ category, productId }) { return this.http .get(`${this.BASE_URL}/${category}/product/${productId}`) .map(res => res.json()); } submitProduct(product) { return this.http .post(`${this.BASE_URL}/product`, product) .map(res => res.json()); } }
- Import
WebService
,HTTPModule
,RouterModule
in the core.module.tsimport { RouterModule } from '@angular/router'; import { HttpModule } from '@angular/http'; import { WebService } from './services/web.service';
- Add
WebService
in theproviders
array.`providers: [WebService],`
- Create
Models
for the application in thecore
moduleExercise Model
- Createcore/models/exercise.model.ts
export interface Exercise { title: string; description: string; }
Category Model
- Createcore/models/category.model.ts
export interface Category { categoryName: string; }
Product Model
- Createcore/models/product.model.ts
export interface Product { _id: string; title: string; category: string; url: string; description: string; imageHref: string; }
- Create home module
ng g m home
- Create
BannerComponent
to contain title and descriptionng g c home/components/banner
- Create app.routes.ts which contains routes array for the entire application.
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadChildren: './home/home.module#HomeModule'} //lazyloading ];
- Import
routes
in app.module.ts. Also ImportRouterModule
from@angular/router
. Then declare it in theAppModule
import { RouterModule } from '@angular/router'; import { routes } from './app.routes'; ... imports: [RouterModule.forRoot(routes)] ...
- Create home.routes.ts to declare routes for the HomeModule
import { Routes } from '@angular/router'; import { BannerComponent } from './components/banner/banner.component'; export const HomeRoutes: Routes = [ { path: '', component: BannerComponent } ];
- Add
HomeRoutes
in the home.module.tsimport { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { BannerComponent } from './components/banner/banner.component'; import { RouterModule } from '@angular/router'; import { HomeRoutes as routes } from './home.routes'; @NgModule({ imports: [ SharedModule, RouterModule.forChild(routes) ], declarations: [BannerComponent] }) export class HomeModule { }
- Add
router-outlet
in app.component.html to render the components depending upon the routes<app-header></app-header> <router-outlet></router-outlet>
- Add
actions
folder in home. Inside actions folder, create home.actions.ts file.import { Action } from '@ngrx/store'; export const GET_TITLE_AND_DESCRIPTION = 'GET_TITLE_AND_DESCRIPTION'; export const GET_TITLE_AND_DESCRIPTION_SUCCESS = 'GET_TITLE_AND_DESCRIPTION_SUCCESS'; export const GET_TITLE_AND_DESCRIPTION_FAILED = 'GET_TITLE_AND_DESCRIPTION_FAILED'; export class GetTitleAndDescription implements Action { readonly type = GET_TITLE_AND_DESCRIPTION; } export class GetTitleAndDescriptionSuccess implements Action { readonly type = GET_TITLE_AND_DESCRIPTION_SUCCESS; constructor(public payload: any) {} } export class GetTitleAndDescriptionFailed implements Action { readonly type = GET_TITLE_AND_DESCRIPTION_FAILED; constructor(public payload: any) {} } export type All = GetTitleAndDescription | GetTitleAndDescriptionFailed | GetTitleAndDescriptionSuccess;
- Add
state
folder in thehome
. Createhome.state.ts
import { Exercise } from '../../core/models/exercise.model'; export interface HomeState { exercise: Exercise; loading: boolean; error: string; }
- Add
reducers
folder in thehome
. Createhome.reducers.ts
.import * as HomeActions from '../actions/home.actions'; import { HomeState } from '../state/home.state'; import { Exercise } from '../../core/models/exercise.model'; export type Action = HomeActions.All; const initialState: HomeState = { exercise: {} as Exercise, loading: false, error: null }; export function homeReducer(state: HomeState = initialState, action: Action) { switch (action.type) { case HomeActions.GET_TITLE_AND_DESCRIPTION: { return { ...state, loading: true }; } case HomeActions.GET_TITLE_AND_DESCRIPTION_SUCCESS: { return { ...state, loading: false, exercise: action.payload }; } case HomeActions.GET_TITLE_AND_DESCRIPTION_FAILED: { return { ...state, loading: false, error: action.payload, exercise: {} as Exercise }; } default: { return state; } } }
- Add
selectors.ts
to create function which will return the home state from the store.import { createSelector, createFeatureSelector } from '@ngrx/store'; import { HomeState } from './home.state'; export const selectHomeState = createFeatureSelector<HomeState>('home');
- Create
app.state.ts
to store the store at the app level.import { HomeState } from './home/state/home.state'; export interface AppState { home: HomeState; }
- Create
app.reducers.ts
import { combineReducers, compose } from '@ngrx/store'; import { AppState } from './app.state'; import { storeFreeze } from 'ngrx-store-freeze'; import { homeReducer } from './home/reducers/home.reducers'; export const reducers = { home: homeReducer }; export const metaReducers = [ storeFreeze ];
- Add
reducers
in theAppModule
import { reducers, metaReducers } from './app.reducers';
- Add
reducers
andmetaReducers
toStoreModule
inAppModule
StoreModule.forRoot(reducers, { metaReducers }),
- Add
home.effects.ts
to listen to theGET_TITLE_AND_DESCRIPTION
action.import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; import { Action, Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { AppState } from '../../app.state'; import { WebService } from '../../core/services/web.service'; import * as HomeActions from '../actions/home.actions'; @Injectable() export class HomeEffects { @Effect() getTitleAndDescription$: Observable<HomeActions.All> = this.actions .ofType(HomeActions.GET_TITLE_AND_DESCRIPTION) .switchMap((action: HomeActions.All) => this.webService.getTitleAndDescription()) .map((data: any) => (new HomeActions.GetTitleAndDescriptionSuccess(data))); constructor( private store: Store<AppState>, private webService: WebService, private actions: Actions ) {} }
- Add
HomeEffects
to theHomeModule
import { EffectsModule } from '@ngrx/effects'; import { HomeEffects } from './effects/home.effects';
imports: [ EffectsModule.forFeature([HomeEffects])],
- Update banner.component.ts to dispatch
GET_TITLE_AND_DESCRIPTION
action and subscribe to data retrieved.import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '../../../app.state'; import { selectHomeState } from '../../state/selectors'; import { Exercise } from '../../../core/models/exercise.model'; import * as HomeActions from '../../actions/home.actions'; @Component({ selector: 'app-banner', templateUrl: './banner.component.html', styleUrls: ['./banner.component.css'] }) export class BannerComponent implements OnInit { exercise: Exercise; loading: boolean; error: string; constructor(private store: Store<AppState>) {} ngOnInit() { this.store.dispatch(new HomeActions.GetTitleAndDescription()); this.store.select(selectHomeState).subscribe(state => { this.exercise = state.exercise; this.loading = state.loading; this.error = state.error; }); } }
- Update banner.component.html to consume the data retrieved.
<md-card *ngIf="!loading"> <md-card-header> <md-card-title>{{ exercise.title }}</md-card-title> </md-card-header> <md-card-content> <p>{{ exercise.description }}</p> </md-card-content> <md-card-actions> <button md-button routerLink="/category/list"> <md-icon>dashboard</md-icon> <span>Categories</span> </button> <button md-button routerLink="/category/add"> <md-icon>add</md-icon> <span>Add Product</span> </button> </md-card-actions> </md-card>
- Create
Category
module using @angular-cling g m category
- Create
category-list
component which will contain the list of all the categories available.ng g c category/components/category-list
- Create
product-list
component which will contain the list of all the products available in a category.ng g c category/components/product-list
- Create
product-form
component to add product.ng g c category/components/product-form
- Create
product-details
component which will contain the details of selected product.ng g c category/components/product-details
- Update app.routes.ts to store a path for
CategoryModule
{ path: 'category', loadChildren: './category/category.module#CategoryModule'}
- Create category.routes.ts to cantain paths for different components - CategoryListComponent, ProductListComponent, ProductDetailsComponent
import { Routes } from '@angular/router'; import { CategoryListComponent } from './components/category-list/category-list.component'; import { ProductListComponent } from './components/product-list/product-list.component'; import { ProductDetailsComponent } from './components/product-details/product-details.component'; import { ProductFormComponent } from './components/product-form/product-form.component'; export const CategoryRoutes: Routes = [ { path: 'list', component: CategoryListComponent }, { path: 'list/:category', component: ProductListComponent }, { path: 'list/:category/:id', component: ProductDetailsComponent }, { path: 'add', component: ProductFormComponent }, ];
- Update
CategoryModule
to contain theroutes
andSharedModule
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { CategoryListComponent } from './components/category-list/category-list.component'; import { ProductListComponent } from './components/product-list/product-list.component'; import { ProductDetailsComponent } from './components/product-details/product-details.component'; import { ProductFormComponent } from './components/product-form/product-form.component'; import { CategoryRoutes as routes } from './category.routes'; @NgModule({ imports: [ SharedModule, RouterModule.forChild(routes) ], declarations: [CategoryListComponent, ProductListComponent, ProductDetailsComponent, ProductFormComponent] }) export class CategoryModule { }
- Create
action
files for all four components- app/category/actions/category.actions.ts
import { Action } from '@ngrx/store'; export const GET_ALL_CATEGORIES = 'GET_ALL_CATEGORIES'; export const GET_ALL_CATEGORIES_SUCCESS = 'GET_ALL_CATEGORIES_SUCCESS'; export const GET_ALL_CATEGORIES_FAILED = 'GET_ALL_CATEGORIES_FAILED'; export class GetAllCategories implements Action { readonly type = GET_ALL_CATEGORIES; } export class GetAllCategoriesSuccess implements Action { readonly type = GET_ALL_CATEGORIES_SUCCESS; constructor(public payload: any) {} } export class GetAllCategoriesFailed implements Action { readonly type = GET_ALL_CATEGORIES_FAILED; constructor(public payload: any) {} } export type All = GetAllCategories | GetAllCategoriesSuccess | GetAllCategoriesFailed;
- app/category/actions/product-list.actions.ts
import { Action } from '@ngrx/store'; export const GET_ALL_PRODUCTS = 'GET_ALL_PRODUCTS'; export const GET_ALL_PRODUCTS_SUCCESS = 'GET_ALL_PRODUCTS_SUCCESS'; export const GET_ALL_PRODUCTS_FAILED = 'GET_ALL_PRODUCTS_FAILED'; export class GetAllProducts implements Action { readonly type = GET_ALL_PRODUCTS; constructor(public payload: any) {} } export class GetAllProductsSuccess implements Action { readonly type = GET_ALL_PRODUCTS_SUCCESS; constructor(public payload: any) {} } export class GetAllProductsFailed implements Action { readonly type = GET_ALL_PRODUCTS_FAILED; constructor(public payload: any) {} } export type All = GetAllProducts | GetAllProductsSuccess | GetAllProductsFailed;
- app/category/actions/product.actions.ts
import { Action } from '@ngrx/store'; export const GET_PRODUCT = 'GET_PRODUCT'; export const GET_PRODUCT_SUCCESS = 'GET_PRODUCT_SUCCESS'; export const GET_PRODUCT_FAILED = 'GET_PRODUCT_FAILED'; export class GetProduct implements Action { readonly type = GET_PRODUCT; constructor(public payload: any) {} } export class GetProductSuccess implements Action { readonly type = GET_PRODUCT_SUCCESS; constructor(public payload: any) {} } export class GetProductFailed implements Action { readonly type = GET_PRODUCT_FAILED; constructor(public payload: any) {} } export type All = GetProduct | GetProductSuccess | GetProductFailed;
- app/category/actions/product-form.actions.ts
import { Action } from '@ngrx/store'; export const SUBMIT_PRODUCT = 'SUBMIT_PRODUCT'; export const SUBMIT_PRODUCT_SUCCESS = 'SUBMIT_PRODUCT_SUCCESS'; export const SUBMIT_PRODUCT_FAILED = 'SUBMIT_PRODUCT_FAILED'; export class SubmitProduct implements Action { readonly type = SUBMIT_PRODUCT; constructor(public payload: any) {} } export class SubmitProductSuccess implements Action { readonly type = SUBMIT_PRODUCT_SUCCESS; constructor(public payload: any) {} } export class SubmitProductFailed implements Action { readonly type = SUBMIT_PRODUCT_FAILED; constructor(public payload: any) {} } export type All = SubmitProduct | SubmitProductSuccess | SubmitProductFailed;
- Create state files for the 4 components
- app/category/state/category.state.ts
import { Category } from '../../core/models/category.model'; export interface CategoryState { categories: Category[]; loading: boolean; error: string; }
- app/category/state/product-list.state.ts
import { Product } from '../../core/models/product.model'; export interface ProductListState { products: Product[]; loading: boolean; error: string; }
- app/category/state/product.state.ts
import { Product } from '../../core/models/product.model'; export interface ProductState { product: Product[]; loading: boolean; error: string; }
- app/category/state/product-form.state.ts
import { Product } from '../../core/models/product.model'; export interface ProductFormState { loading: boolean; error: string; }
- Create selectors to select the appropriate state from our compoenents selectors.ts
import { createFeatureSelector } from '@ngrx/store'; import { CategoryState } from './category.state'; import { ProductState } from './product.state'; import { ProductListState } from './product-list.state'; import { ProductFormState } from './product-form.state'; export const selectCategories = createFeatureSelector<CategoryState>('categories'); export const selectProducts = createFeatureSelector<ProductListState>('products'); export const selectProduct = createFeatureSelector<ProductState>('product'); export const selectProductForm = createFeatureSelector<ProductFormState>('form');
- Create reducers for the four components
- app/category/reducers/category.reducers.ts
import * as CategoryActions from '../actions/category.actions'; import { Category } from '../../core/models/category.model'; import { CategoryState } from '../state/category.state'; const initialState: CategoryState = { categories: [] as Category[], loading: false, error: null }; export type Action = CategoryActions.All; export function categoryReducers (state: CategoryState = initialState, action: Action) { switch (action.type) { case CategoryActions.GET_ALL_CATEGORIES: { return { ...state, loading: true }; } case CategoryActions.GET_ALL_CATEGORIES_SUCCESS: { return { ...state, loading: false, categories: action.payload }; } case CategoryActions.GET_ALL_CATEGORIES_FAILED: { return { ...state, loading: false, categories: [] as Category[], error: action.payload }; } default: return state; } }
- app/category/reducers/product-list.reducers.ts
import * as ProductActions from '../actions/product-list.actions'; import { Product } from '../../core/models/product.model'; import { ProductListState } from '../state/product-list.state'; const initialState: ProductListState = { products: [] as Product[], loading: false, error: null }; export type Action = ProductActions.All; export function categoryReducers (state: ProductListState = initialState, action: Action) { switch (action.type) { case ProductActions.GET_ALL_PRODUCTS: { return { ...state, loading: true }; } case ProductActions.GET_ALL_PRODUCTS_SUCCESS: { return { ...state, loading: false, products: action.payload }; } case ProductActions.GET_ALL_PRODUCTS_FAILED: { return { ...state, loading: false, products: [] as Product[], error: action.payload }; } default: return state; } }
- app/category/reducers/product.reducers.ts
import * as ProductActions from '../actions/product.actions'; import { Product } from '../../core/models/product.model'; import { ProductState } from '../state/product.state'; const initialState: ProductState = { product: [] as Product[], loading: false, error: null }; export type Action = ProductActions.All; export function categoryReducers (state: ProductState = initialState, action: Action) { switch (action.type) { case ProductActions.GET_PRODUCT: { return { ...state, loading: true }; } case ProductActions.GET_PRODUCT_SUCCESS: { return { ...state, loading: false, product: action.payload }; } case ProductActions.GET_PRODUCT_FAILED: { return { ...state, loading: false, product: [] as Product[], error: action.payload }; } default: return state; } }
- app/category/reducers/product-form.reducers.ts
import * as ProductFormActions from '../actions/product-form.actions'; import { ProductFormState } from '../state/product-form.state'; const initialState: ProductFormState = { loading: false, error: null }; export type Action = ProductFormActions.All; export function productReducers (state: ProductFormState = initialState, action: Action) { switch (action.type) { case ProductFormActions.SUBMIT_PRODUCT: { return { ...state, loading: true }; } case ProductFormActions.SUBMIT_PRODUCT_SUCCESS: { return { ...state, loading: false }; } case ProductFormActions.SUBMIT_PRODUCT_FAILED: { return { ...state, loading: false, error: action.payload }; } default: return state; } }
- Update
AppState
to accomodate newly added state.import { HomeState } from './home/state/home.state'; import { CategoryState } from './category/state/category.state'; import { ProductListState } from './category/state/product-list.state'; import { ProductState } from './category/state/product.state'; import { ProductFormState } from './category/state/product-form.state'; export interface AppState { home: HomeState; categories: CategoryState; products: ProductListState; product: ProductState; form: ProductFormState; }
- Update
app.reducers.ts
to accomodate newly added reducersimport { categoryReducers } from './category/reducers/category.reducers'; import { productListReducers } from './category/reducers/product-list.reducers'; import { productReducers } from './category/reducers/product.reducers'; import { productFormReducers } from './category/reducers/product-form.reducers'; export const reducers = { home: homeReducer, categories: categoryReducers, products: productListReducers, product: productReducers, form: productFormReducers };
- Create effects file category.effects.ts to listen to events
import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; import { Action, Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { Router } from '@angular/router'; import * as CategoryActions from '../actions/category.actions'; import * as ProductListActions from '../actions/product-list.actions'; import * as ProductActions from '../actions/product.actions'; import * as ProductFormActions from '../actions/product-form.actions'; import { WebService } from '../../core/services/web.service'; import { AppState } from '../../app.state'; @Injectable() export class CategoryEffects { @Effect() getCategories$: Observable<Action> = this.actions$ .ofType(CategoryActions.GET_ALL_CATEGORIES) .switchMap((action: CategoryActions.All) => this.webService.getCategories()) .map((data: any) => (new CategoryActions.GetAllCategoriesSuccess(data))); @Effect() getProducts$: Observable<Action> = this.actions$ .ofType(ProductListActions.GET_ALL_PRODUCTS) .switchMap((action: ProductListActions.All) => this.webService.getProducts(action.payload)) .map((data: any) => (new ProductListActions.GetAllProductsSuccess(data))); @Effect() getProduct$: Observable<Action> = this.actions$ .ofType(ProductActions.GET_PRODUCT) .switchMap((action: ProductActions.All) => this.webService.getProduct(action.payload)) .map((data: any) => (new ProductActions.GetProductSuccess(data))); @Effect({ dispatch: false }) submitProduct$: Observable<Action> = this.actions$ .ofType(ProductFormActions.SUBMIT_PRODUCT) .switchMap((action: ProductFormActions.All) => this.webService.submitProduct(action.payload)) .do(() => this.router.navigate(['category', 'list'])); constructor( private store: Store<AppState>, private actions$: Actions, private webService: WebService, private router: Router ) {} }
- Add
CategoryEffects
in theCategoryModule
import { EffectsModule } from '@ngrx/effects'; import { CategoryEffects } from './effects/category.effects';
imports: [ EffectsModule.forFeature([CategoryEffects]) ]
- Update
component-list.component.ts
to dispatchGET_ALL_CATEGORIES
action.import { Component, OnInit, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; import * as CategoryActions from '../../actions/category.actions'; import { selectCategories } from '../../state/selectors'; import { AppState } from '../../../app.state'; import { Category } from '../../../core/models/category.model'; @Component({ selector: 'app-category-list', templateUrl: './category-list.component.html', styleUrls: ['./category-list.component.css'] }) export class CategoryListComponent implements OnInit, OnDestroy { categories: Category[]; loading: boolean; error: string; storeSubscription; constructor(private store: Store<AppState>) {} ngOnInit() { this.store.dispatch(new CategoryActions.GetAllCategories()); this.storeSubscription = this.store.select(selectCategories).subscribe(state => { this.categories = state.categories; this.loading = state.loading; this.error = state.error; }); } ngOnDestroy() { this.storeSubscription.unsubscribe(); } }
- Update
category-list.component.html
accordingly.<md-card *ngIf="!!categories.length"> <md-nav-list> <h3 md-subheader>Categories</h3> <md-list-item mdTooltip="Click to go to {{category.categoryName}}" mdTooltipPosition="below" [routerLink]="['/category','list',category.categoryName]" *ngFor="let category of categories"> <md-icon md-list-icon>widgets</md-icon> <h4 md-line>{{ category.categoryName }}</h4> </md-list-item> </md-nav-list> </md-card>
- Update
product-list.component.ts
to dispatchGET_ALL_PRODUCTS
action and display product list belonging to particular categoryimport { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { AppState } from '../../../app.state'; import { Product } from '../../../core/models/product.model'; import * as ProductListActions from '../../actions/product-list.actions'; import { selectProducts } from '../../state/selectors'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit, OnDestroy { products: Product[]; loading: boolean; error: string; routeSubscription; storeSubscription; constructor( private store: Store<AppState>, private route: ActivatedRoute) { } ngOnInit() { this.routeSubscription = this.route.params.subscribe(params => { const category = params.category; this.store.dispatch(new ProductListActions.GetAllProducts(category)); this.storeSubscription = this.store.select(selectProducts).subscribe(state => { this.products = state.products; this.loading = state.loading; this.error = state.error; }); }); } ngOnDestroy () { this.routeSubscription.unsubscribe(); this.storeSubscription.unsubscribe(); } }
- Update
product-list.component.html
to display the product-list<div class="product-list"> <md-card mdTooltip="Click to go to details page" mdTooltipPosition="above" [routerLink]='["/category","list",product.category,product._id]' *ngFor="let product of products"> <md-card-header> <div md-card-avatar class="avatar"> {{ product.title.split("")[0] }}</div> <md-card-title>{{product.title}}</md-card-title> <md-card-subtitle>{{product.category}}</md-card-subtitle> </md-card-header> <div md-card-image class="product-image" [ngStyle]="{backgroundImage: 'url('+product.imageHref+')'}"></div> <md-card-content> <p> {{product.description}} </p> <p> <md-icon>open in new</md-icon> <a href="{{product.url}}">{{product.url}}</a> </p> </md-card-content> </md-card> </div>
- Update
product-details.component.ts
to dispatchGET_PRODUCT
action and display product details of the selected productimport { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import * as ProductActions from '../../actions/product.actions'; import { AppState } from '../../../app.state'; import { Product } from '../../../core/models/product.model'; import { selectProduct } from '../../state/selectors'; @Component({ selector: 'app-product-details', templateUrl: './product-details.component.html', styleUrls: ['./product-details.component.css'] }) export class ProductDetailsComponent implements OnInit, OnDestroy { product: Product; loading: boolean; error: string; routeSubscription; stateSubscription; constructor( private route: ActivatedRoute, private store: Store<AppState> ) { } ngOnInit() { this.routeSubscription = this.route.params.subscribe(params => { const category = params.category; const productId = params.id; this.store.dispatch(new ProductActions.GetProduct({ category, productId})); this.stateSubscription = this.store.select(selectProduct).subscribe(state => { this.product = state.product[0]; this.loading = state.loading; this.error = state.error; }); }); } ngOnDestroy() { this.routeSubscription.unsubscribe(); this.stateSubscription.unsubscribe(); } }
- Update
product-list.component.html
to display the product details<md-card class="product-card" *ngIf="!!product"> <div class="product-image" [ngStyle]="{backgroundImage: 'url('+product.imageHref+')'}"></div> <div class="product-info"> <md-card-header> <div md-card-avatar class="avatar"> {{ product.title.split("")[0] }}</div> <md-card-title>{{product.title}}</md-card-title> <md-card-subtitle>{{product.category}}</md-card-subtitle> </md-card-header> <md-card-content> <p> {{product.description}} </p> <p> <md-icon>open in new</md-icon> <a href="{{product.url}}">{{product.url}}</a> </p> </md-card-content> </div> </md-card>
- Update
product-form.component.ts
to dispatch submit action when form is submitted.import { Component, OnInit, OnDestroy } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '../../../app.state'; import * as ProductFormActions from '../../actions/product-form.actions'; import { selectProductForm } from '../../state/selectors'; @Component({ selector: 'app-product-form', templateUrl: './product-form.component.html', styleUrls: ['./product-form.component.css'] }) export class ProductFormComponent implements OnInit, OnDestroy { form; storeSubscription; loading: boolean; error: string; constructor( private fb: FormBuilder, private store: Store<AppState> ) { this.form = this.fb.group({ category: ['', Validators.required], title: ['', Validators.required], url: ['', Validators.required], description: ['', Validators.required], imageHref: ['', Validators.required] }); } ngOnInit() { this.storeSubscription = this.store.select(selectProductForm).subscribe(state => { this.loading = state.loading; this.error = state.error; }); } ngOnDestroy() { this.storeSubscription.unsubscribe(); } onSubmit() { if (this.form.valid) { this.store.dispatch(new ProductFormActions.SubmitProduct(this.form.value)); } } }
- Update
product.form.component.html
to display form to enter product details<md-card> <h3 md-header>Add Product</h3> <form [formGroup]="form" (ngSubmit) = "onSubmit()"> <p> <md-input-container> <input type="text" mdInput formControlName="category" placeholder="Category"> </md-input-container> </p> <p> <md-input-container> <input type="text" mdInput formControlName="title" placeholder="Title"> </md-input-container> </p> <p> <md-input-container> <input type="url" mdInput formControlName="url" placeholder="URL"> </md-input-container> </p> <p> <md-input-container> <textarea type="text" mdInput formControlName="description" placeholder="Description"></textarea> </md-input-container> </p> <p> <md-input-container> <input type="url" mdInput formControlName="imageHref" placeholder="Image URL"> </md-input-container> </p> <p [ngStyle]="{ textAlign: 'center' }"> <button md-raised-button color="primary"> Submit </button> </p> </form> </md-card>
This finishes the tutorial to create the application using angular 2+ and redux. Now run the application in the browser using npm start
- Created tutorial for angular 2 and ngrx
- Integrate Unit test cases
- Integration test cases
- E2E test cases
- Fork the project
- git clone
- git checkout -b
- git push origin
- Raise PR for the feature