Skip to content

Commit 7eb155d

Browse files
committed
feat: stock market example
1 parent fd427a3 commit 7eb155d

21 files changed

+335
-25
lines changed

src/app/core/core.module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
3+
import { HttpModule } from '@angular/http';
34
import { StoreModule, combineReducers, ActionReducer } from '@ngrx/store';
4-
import { EffectsModule } from '@ngrx/effects';
55

6-
import { settingsReducer, SettingsEffects } from '../settings';
6+
import { settingsReducer } from '../settings';
77

88
import { LocalStorageService } from './local-storage/local-storage.service';
99
import {
1010
localStorageInitStateMiddleware
1111
} from './local-storage/local-storage.middleware';
1212

13-
1413
export function createReducer(asyncReducers = {}): ActionReducer<any> {
1514
return localStorageInitStateMiddleware(
1615
combineReducers(Object.assign({
@@ -28,6 +27,7 @@ export function reducerAoT(state, action) {
2827
@NgModule({
2928
imports: [
3029
CommonModule,
30+
HttpModule,
3131
StoreModule.provideStore(reducerAoT)
3232
],
3333
declarations: [],

src/app/examples/examples.module.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,36 @@ import { CoreModule, createReducer } from '../core';
66
import { SharedModule } from '../shared';
77

88
import { ExamplesRoutingModule } from './examples-routing.module';
9-
import { TodosComponent } from './todos/todos.component';
109
import { ExamplesComponent } from './examples/examples.component';
11-
import { StockMarketComponent } from './stock-market/stock-market.component';
12-
import { StockMarketService } from './stock-market/stock-market.service';
13-
10+
import { TodosComponent } from './todos/todos.component';
1411
import { todosReducer } from './todos/todos.reducer';
1512
import { TodosEffects } from './todos/todos.effects';
13+
import { StockMarketComponent } from './stock-market/stock-market.component';
14+
import { stockMarketReducer } from './stock-market/stock-market.reducer';
15+
import { StockMarketEffects } from './stock-market/stock-market.effects';
16+
import { StockMarketService } from './stock-market/stock-market.service';
1617

1718
export const appReducerWithExamples = createReducer({
18-
todos: todosReducer
19+
todos: todosReducer,
20+
stocks: stockMarketReducer
1921
});
2022

2123
@NgModule({
2224
imports: [
2325
CoreModule,
2426
SharedModule,
2527
ExamplesRoutingModule,
26-
EffectsModule.run(TodosEffects)
28+
EffectsModule.run(TodosEffects),
29+
EffectsModule.run(StockMarketEffects)
30+
],
31+
declarations: [
32+
ExamplesComponent,
33+
TodosComponent,
34+
StockMarketComponent
2735
],
28-
declarations: [TodosComponent, ExamplesComponent, StockMarketComponent],
29-
providers: [StockMarketService]
36+
providers: [
37+
StockMarketService
38+
]
3039
})
3140
export class ExamplesModule {
3241

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,61 @@
1-
<p>
2-
stock-market works!
3-
</p>
1+
<div class="container">
2+
<div class="row">
3+
<div class="col-md-12">
4+
<h1 class="main-heading">Stock Market</h1>
5+
</div>
6+
</div>
7+
<div class="row">
8+
<div class="col-md-6 col-lg-3">
9+
<form autocomplete="false">
10+
<md-input-container>
11+
<input mdInput placeholder="Stock symbol"
12+
[value]="stocks.symbol"
13+
(keyup)="onSymbolChange($event.target.value)">
14+
</md-input-container>
15+
</form>
16+
<p>
17+
Please provide some valid stock market symbol like: GOOGL, FB, AAPL, NVDA, AMZN, TWTR, SNAP, TSLA...
18+
</p>
19+
<br>
20+
</div>
21+
<div class="col-md-6 col-lg-4 offset-lg-1">
22+
<md-spinner *ngIf="stocks.loading"></md-spinner>
23+
<md-card *ngIf="stocks.stock">
24+
<md-card-title>{{stocks.stock.symbol}} <span>{{stocks.stock.last}} {{stocks.stock.ccy}}</span></md-card-title>
25+
<md-card-subtitle>
26+
{{stocks.stock.exchange}}
27+
<span [ngClass]="{ negative: stocks.stock.changeNegative }">
28+
<i class="fa fa-caret-up" *ngIf="stocks.stock.changePositive"></i>
29+
<i class="fa fa-caret-down" *ngIf="stocks.stock.changeNegative"></i>
30+
{{stocks.stock.change}} ({{stocks.stock.changePercent}})
31+
</span>
32+
</md-card-subtitle>
33+
</md-card>
34+
<p *ngIf="stocks.error" class="error">
35+
<i class="fa fa-exclamation-triangle fa-3x" aria-hidden="true"></i><br><br>
36+
<span>Stock <span class="symbol">{{stocks.symbol}}</span> not found</span>
37+
</p>
38+
<br>
39+
<br>
40+
</div>
41+
<div class="col-md-12 col-lg-4">
42+
<p>
43+
Stock market example shows how to implement <code>HTTP</code>
44+
requests using <code>@ngrx/effects</code> module.
45+
</p>
46+
<p>
47+
Updating symbol query with different symbol will emit action
48+
which updates state with loading flag (reducer) and triggers effect for retrieving
49+
of selected stock.
50+
</p>
51+
<p>
52+
Actions are debounced and every subsequent request will
53+
cancel previous one using <code>.switchMap</code>.
54+
</p>
55+
<p>
56+
Success or error actions are emitted on request completion.
57+
Loading spinner is removed and stock info or error message is displayed.
58+
</p>
59+
</div>
60+
</div>
61+
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@import '~@angular/material/theming';
2+
3+
@mixin todos-component-theme($theme) {
4+
$warn: map-get($theme, warn);
5+
6+
md-card {
7+
span {
8+
&.negative {
9+
color: mat-color($warn);
10+
}
11+
}
12+
}
13+
.error {
14+
i {
15+
color: mat-color($warn);
16+
}
17+
}
18+
}
19+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.main-heading {
2+
text-transform: uppercase;
3+
margin: 0 0 20px 0
4+
}
5+
6+
md-input-container {
7+
width: 100%
8+
}
9+
10+
md-card {
11+
span {
12+
float: right;
13+
14+
i {
15+
margin: 0 5px 0 0
16+
}
17+
}
18+
}
19+
20+
21+
md-spinner {
22+
margin: auto;
23+
}
24+
25+
.error {
26+
text-align: center;
27+
padding: 20px;
28+
29+
>span {
30+
opacity: 0.4;
31+
}
32+
.symbol {
33+
font-weight: bold;
34+
}
35+
}

src/app/examples/stock-market/stock-market.component.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
3+
4+
import { CoreModule } from '../../core';
5+
import { SharedModule } from '../../shared';
6+
import { ExamplesModule } from '../examples.module';
27

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

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

914
beforeEach(async(() => {
1015
TestBed.configureTestingModule({
11-
declarations: [ StockMarketComponent ]
16+
imports: [
17+
NoopAnimationsModule,
18+
CoreModule,
19+
SharedModule,
20+
ExamplesModule
21+
]
1222
})
1323
.compileComponents();
1424
}));
Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,49 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, OnInit, OnDestroy } from '@angular/core';
2+
import { Store } from '@ngrx/store';
3+
import { Subject } from 'rxjs/Subject';
4+
import 'rxjs/add/operator/takeUntil';
5+
import 'rxjs/add/operator/map';
6+
7+
import { retrieveStock } from './stock-market.reducer';
28

39
@Component({
410
selector: 'anms-stock-market',
511
templateUrl: './stock-market.component.html',
612
styleUrls: ['./stock-market.component.scss']
713
})
8-
export class StockMarketComponent implements OnInit {
14+
export class StockMarketComponent implements OnInit, OnDestroy {
15+
16+
private unsubscribe$: Subject<void> = new Subject<void>();
917

10-
constructor() { }
18+
initialized;
19+
stocks;
20+
21+
constructor(
22+
public store: Store<any>
23+
) {}
1124

1225
ngOnInit() {
26+
this.initialized = false;
27+
this.store
28+
.select('stocks')
29+
.takeUntil(this.unsubscribe$)
30+
.subscribe((stocks: any) => {
31+
this.stocks = stocks;
32+
33+
if (!this.initialized) {
34+
this.initialized = true;
35+
this.store.dispatch(retrieveStock(stocks.symbol));
36+
}
37+
});
38+
}
39+
40+
ngOnDestroy(): void {
41+
this.unsubscribe$.next();
42+
this.unsubscribe$.complete();
43+
}
44+
45+
onSymbolChange(symbol: string) {
46+
this.store.dispatch(retrieveStock(symbol));
1347
}
1448

1549
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Injectable } from '@angular/core';
2+
import { Actions, Effect } from '@ngrx/effects';
3+
import { Action } from '@ngrx/store';
4+
import { Observable } from 'rxjs/Observable';
5+
import 'rxjs/add/operator/map';
6+
import 'rxjs/add/operator/debounceTime';
7+
import 'rxjs/add/operator/distinctUntilChanged';
8+
import 'rxjs/add/observable/of';
9+
10+
import { LocalStorageService } from '../../core';
11+
12+
import {
13+
STOCK_MARKET_KEY,
14+
STOCK_MARKET_RETRIEVE,
15+
STOCK_MARKET_RETRIEVE_SUCCESS,
16+
STOCK_MARKET_RETRIEVE_ERROR
17+
} from './stock-market.reducer';
18+
import { StockMarketService } from './stock-market.service';
19+
20+
@Injectable()
21+
export class StockMarketEffects {
22+
23+
constructor(
24+
private actions$: Actions,
25+
private localStorageService: LocalStorageService,
26+
private service: StockMarketService
27+
) {}
28+
29+
@Effect() retrieveStock(): Observable<Action> {
30+
return this.actions$
31+
.ofType(STOCK_MARKET_RETRIEVE)
32+
.do(action => this.localStorageService
33+
.setItem(STOCK_MARKET_KEY, { symbol: action.payload }))
34+
.distinctUntilChanged()
35+
.debounceTime(500)
36+
.switchMap(action =>
37+
this.service.retrieveStock(action.payload)
38+
.map(stock =>
39+
({ type: STOCK_MARKET_RETRIEVE_SUCCESS, payload: stock }))
40+
.catch(err =>
41+
Observable.of({ type: STOCK_MARKET_RETRIEVE_ERROR, payload: err }))
42+
);
43+
}
44+
45+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Action } from '@ngrx/store';
2+
3+
export const initialState = {
4+
symbol: 'GOOGL'
5+
};
6+
7+
export const STOCK_MARKET_KEY = 'STOCKS';
8+
export const STOCK_MARKET_RETRIEVE = 'STOCK_MARKET_RETRIEVE';
9+
export const STOCK_MARKET_RETRIEVE_SUCCESS = 'STOCK_MARKET_RETRIEVE_SUCCESS';
10+
export const STOCK_MARKET_RETRIEVE_ERROR = 'STOCK_MARKET_RETRIEVE_ERROR';
11+
12+
export const retrieveStock = (symbol: string) =>
13+
({ type: STOCK_MARKET_RETRIEVE, payload: symbol });
14+
15+
export function stockMarketReducer(state = initialState, action: Action) {
16+
switch (action.type) {
17+
case STOCK_MARKET_RETRIEVE:
18+
return Object.assign({}, state, {
19+
loading: true,
20+
stock: null,
21+
error: null,
22+
symbol: action.payload,
23+
});
24+
25+
case STOCK_MARKET_RETRIEVE_SUCCESS:
26+
return Object.assign({}, state, {
27+
loading: false,
28+
stock: action.payload,
29+
error: null
30+
});
31+
32+
case STOCK_MARKET_RETRIEVE_ERROR:
33+
return Object.assign({}, state, {
34+
loading: false,
35+
stock: null,
36+
error: action.payload
37+
});
38+
39+
default:
40+
return state;
41+
}
42+
}
43+
44+
export interface Stock {
45+
symbol: string;
46+
exchange: string;
47+
last: string;
48+
ccy: string;
49+
change: string;
50+
}

src/app/examples/stock-market/stock-market.service.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { TestBed, inject } from '@angular/core/testing';
22

3+
import { CoreModule } from '../../core';
4+
35
import { StockMarketService } from './stock-market.service';
46

57
describe('StockMarketService', () => {
68
beforeEach(() => {
79
TestBed.configureTestingModule({
8-
providers: [StockMarketService]
10+
imports: [
11+
CoreModule,
12+
],
13+
providers: [
14+
StockMarketService
15+
]
916
});
1017
});
1118

0 commit comments

Comments
 (0)