Skip to content

Commit

Permalink
feat: stock market example
Browse files Browse the repository at this point in the history
  • Loading branch information
tomastrajan committed May 28, 2017
1 parent fd427a3 commit 7eb155d
Show file tree
Hide file tree
Showing 21 changed files with 335 additions and 25 deletions.
6 changes: 3 additions & 3 deletions src/app/core/core.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http';
import { StoreModule, combineReducers, ActionReducer } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

import { settingsReducer, SettingsEffects } from '../settings';
import { settingsReducer } from '../settings';

import { LocalStorageService } from './local-storage/local-storage.service';
import {
localStorageInitStateMiddleware
} from './local-storage/local-storage.middleware';


export function createReducer(asyncReducers = {}): ActionReducer<any> {
return localStorageInitStateMiddleware(
combineReducers(Object.assign({
Expand All @@ -28,6 +27,7 @@ export function reducerAoT(state, action) {
@NgModule({
imports: [
CommonModule,
HttpModule,
StoreModule.provideStore(reducerAoT)
],
declarations: [],
Expand Down
25 changes: 17 additions & 8 deletions src/app/examples/examples.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,36 @@ import { CoreModule, createReducer } from '../core';
import { SharedModule } from '../shared';

import { ExamplesRoutingModule } from './examples-routing.module';
import { TodosComponent } from './todos/todos.component';
import { ExamplesComponent } from './examples/examples.component';
import { StockMarketComponent } from './stock-market/stock-market.component';
import { StockMarketService } from './stock-market/stock-market.service';

import { TodosComponent } from './todos/todos.component';
import { todosReducer } from './todos/todos.reducer';
import { TodosEffects } from './todos/todos.effects';
import { StockMarketComponent } from './stock-market/stock-market.component';
import { stockMarketReducer } from './stock-market/stock-market.reducer';
import { StockMarketEffects } from './stock-market/stock-market.effects';
import { StockMarketService } from './stock-market/stock-market.service';

export const appReducerWithExamples = createReducer({
todos: todosReducer
todos: todosReducer,
stocks: stockMarketReducer
});

@NgModule({
imports: [
CoreModule,
SharedModule,
ExamplesRoutingModule,
EffectsModule.run(TodosEffects)
EffectsModule.run(TodosEffects),
EffectsModule.run(StockMarketEffects)
],
declarations: [
ExamplesComponent,
TodosComponent,
StockMarketComponent
],
declarations: [TodosComponent, ExamplesComponent, StockMarketComponent],
providers: [StockMarketService]
providers: [
StockMarketService
]
})
export class ExamplesModule {

Expand Down
64 changes: 61 additions & 3 deletions src/app/examples/stock-market/stock-market.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@
<p>
stock-market works!
</p>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="main-heading">Stock Market</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-lg-3">
<form autocomplete="false">
<md-input-container>
<input mdInput placeholder="Stock symbol"
[value]="stocks.symbol"
(keyup)="onSymbolChange($event.target.value)">
</md-input-container>
</form>
<p>
Please provide some valid stock market symbol like: GOOGL, FB, AAPL, NVDA, AMZN, TWTR, SNAP, TSLA...
</p>
<br>
</div>
<div class="col-md-6 col-lg-4 offset-lg-1">
<md-spinner *ngIf="stocks.loading"></md-spinner>
<md-card *ngIf="stocks.stock">
<md-card-title>{{stocks.stock.symbol}} <span>{{stocks.stock.last}} {{stocks.stock.ccy}}</span></md-card-title>
<md-card-subtitle>
{{stocks.stock.exchange}}
<span [ngClass]="{ negative: stocks.stock.changeNegative }">
<i class="fa fa-caret-up" *ngIf="stocks.stock.changePositive"></i>
<i class="fa fa-caret-down" *ngIf="stocks.stock.changeNegative"></i>
{{stocks.stock.change}} ({{stocks.stock.changePercent}})
</span>
</md-card-subtitle>
</md-card>
<p *ngIf="stocks.error" class="error">
<i class="fa fa-exclamation-triangle fa-3x" aria-hidden="true"></i><br><br>
<span>Stock <span class="symbol">{{stocks.symbol}}</span> not found</span>
</p>
<br>
<br>
</div>
<div class="col-md-12 col-lg-4">
<p>
Stock market example shows how to implement <code>HTTP</code>
requests using <code>@ngrx/effects</code> module.
</p>
<p>
Updating symbol query with different symbol will emit action
which updates state with loading flag (reducer) and triggers effect for retrieving
of selected stock.
</p>
<p>
Actions are debounced and every subsequent request will
cancel previous one using <code>.switchMap</code>.
</p>
<p>
Success or error actions are emitted on request completion.
Loading spinner is removed and stock info or error message is displayed.
</p>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import '~@angular/material/theming';

@mixin todos-component-theme($theme) {
$warn: map-get($theme, warn);

md-card {
span {
&.negative {
color: mat-color($warn);
}
}
}
.error {
i {
color: mat-color($warn);
}
}
}

35 changes: 35 additions & 0 deletions src/app/examples/stock-market/stock-market.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.main-heading {
text-transform: uppercase;
margin: 0 0 20px 0
}

md-input-container {
width: 100%
}

md-card {
span {
float: right;

i {
margin: 0 5px 0 0
}
}
}


md-spinner {
margin: auto;
}

.error {
text-align: center;
padding: 20px;

>span {
opacity: 0.4;
}
.symbol {
font-weight: bold;
}
}
12 changes: 11 additions & 1 deletion src/app/examples/stock-market/stock-market.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { CoreModule } from '../../core';
import { SharedModule } from '../../shared';
import { ExamplesModule } from '../examples.module';

import { StockMarketComponent } from './stock-market.component';

Expand All @@ -8,7 +13,12 @@ describe('StockMarketComponent', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StockMarketComponent ]
imports: [
NoopAnimationsModule,
CoreModule,
SharedModule,
ExamplesModule
]
})
.compileComponents();
}));
Expand Down
40 changes: 37 additions & 3 deletions src/app/examples/stock-market/stock-market.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/map';

import { retrieveStock } from './stock-market.reducer';

@Component({
selector: 'anms-stock-market',
templateUrl: './stock-market.component.html',
styleUrls: ['./stock-market.component.scss']
})
export class StockMarketComponent implements OnInit {
export class StockMarketComponent implements OnInit, OnDestroy {

private unsubscribe$: Subject<void> = new Subject<void>();

constructor() { }
initialized;
stocks;

constructor(
public store: Store<any>
) {}

ngOnInit() {
this.initialized = false;
this.store
.select('stocks')
.takeUntil(this.unsubscribe$)
.subscribe((stocks: any) => {
this.stocks = stocks;

if (!this.initialized) {
this.initialized = true;
this.store.dispatch(retrieveStock(stocks.symbol));
}
});
}

ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}

onSymbolChange(symbol: string) {
this.store.dispatch(retrieveStock(symbol));
}

}
45 changes: 45 additions & 0 deletions src/app/examples/stock-market/stock-market.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/observable/of';

import { LocalStorageService } from '../../core';

import {
STOCK_MARKET_KEY,
STOCK_MARKET_RETRIEVE,
STOCK_MARKET_RETRIEVE_SUCCESS,
STOCK_MARKET_RETRIEVE_ERROR
} from './stock-market.reducer';
import { StockMarketService } from './stock-market.service';

@Injectable()
export class StockMarketEffects {

constructor(
private actions$: Actions,
private localStorageService: LocalStorageService,
private service: StockMarketService
) {}

@Effect() retrieveStock(): Observable<Action> {
return this.actions$
.ofType(STOCK_MARKET_RETRIEVE)
.do(action => this.localStorageService
.setItem(STOCK_MARKET_KEY, { symbol: action.payload }))
.distinctUntilChanged()
.debounceTime(500)
.switchMap(action =>
this.service.retrieveStock(action.payload)
.map(stock =>
({ type: STOCK_MARKET_RETRIEVE_SUCCESS, payload: stock }))
.catch(err =>
Observable.of({ type: STOCK_MARKET_RETRIEVE_ERROR, payload: err }))
);
}

}
50 changes: 50 additions & 0 deletions src/app/examples/stock-market/stock-market.reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Action } from '@ngrx/store';

export const initialState = {
symbol: 'GOOGL'
};

export const STOCK_MARKET_KEY = 'STOCKS';
export const STOCK_MARKET_RETRIEVE = 'STOCK_MARKET_RETRIEVE';
export const STOCK_MARKET_RETRIEVE_SUCCESS = 'STOCK_MARKET_RETRIEVE_SUCCESS';
export const STOCK_MARKET_RETRIEVE_ERROR = 'STOCK_MARKET_RETRIEVE_ERROR';

export const retrieveStock = (symbol: string) =>
({ type: STOCK_MARKET_RETRIEVE, payload: symbol });

export function stockMarketReducer(state = initialState, action: Action) {
switch (action.type) {
case STOCK_MARKET_RETRIEVE:
return Object.assign({}, state, {
loading: true,
stock: null,
error: null,
symbol: action.payload,
});

case STOCK_MARKET_RETRIEVE_SUCCESS:
return Object.assign({}, state, {
loading: false,
stock: action.payload,
error: null
});

case STOCK_MARKET_RETRIEVE_ERROR:
return Object.assign({}, state, {
loading: false,
stock: null,
error: action.payload
});

default:
return state;
}
}

export interface Stock {
symbol: string;
exchange: string;
last: string;
ccy: string;
change: string;
}
9 changes: 8 additions & 1 deletion src/app/examples/stock-market/stock-market.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { TestBed, inject } from '@angular/core/testing';

import { CoreModule } from '../../core';

import { StockMarketService } from './stock-market.service';

describe('StockMarketService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [StockMarketService]
imports: [
CoreModule,
],
providers: [
StockMarketService
]
});
});

Expand Down

0 comments on commit 7eb155d

Please sign in to comment.