Skip to content

Instantly share code, notes, and snippets.

@peerreynders
Created April 21, 2018 18:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peerreynders/b3821d00175b081d08ea9df55add7d22 to your computer and use it in GitHub Desktop.
Save peerreynders/b3821d00175b081d08ea9df55add7d22 to your computer and use it in GitHub Desktop.
RxJS in Action Ch10 3A: Simple banking form with checking text field and withdraw button
// file: src/index.js - Derived from:
// RxJS in Action (2017, Manning), 978-1-617-29341-2
// by Paul P. Daniels and Luis Atencio
// Listings:
// 10.3 Simple banking form with checking text field and withdraw button (p.289)
//
// $ npm i -P redux
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import registerServiceWorker from './registerServiceWorker';
import { createStore } from 'redux';
const CHECKING = 'checking';
const SAVINGS = 'savings';
const getBalance = (account, state) => state[account];
// action type
const WITHDRAW = 'WITHDRAW';
const DEPOSIT = 'DEPOSIT';
// action creators
const withdraw = (payload) => ({
type: WITHDRAW,
payload
});
const deposit = (payload) => ({ // eslint-disable-line no-unused-vars
type: DEPOSIT,
payload
});
const depositAmount = (balance, amount) => balance + amount;
const withdrawAmount = (balance, amount) => balance - amount;
const updateBalance = (update, {account, amount}, state) => {
return typeof amount !== 'number' | isNaN(amount) || amount <= 0 ?
state :
{...state, [account]: update(getBalance(account, state), amount)};
};
// state updates
const depositTo = (payload, state) =>
updateBalance(depositAmount, payload, state);
const withdrawFrom = (payload, state) =>
updateBalance(withdrawAmount, payload, state);
// reducer
const updateAccounts = (state, action) => {
const {type, payload} = action;
switch(type) {
case WITHDRAW:
return withdrawFrom(payload, state);
case DEPOSIT:
return depositTo(payload, state);
default:
return state;
// eslint-disable-next-line no-unreachable
};
};
const storePropType = PropTypes.shape({
subscribe : PropTypes.func.isRequired,
dispatch : PropTypes.func.isRequired,
getState : PropTypes.func.isRequired
});
// Presentation Components
const locale = 'en-US';
const currency = 'USD';
const balanceFormat =
new Intl.NumberFormat(locale, {style: 'currency', currency});
const AccountBalance = ({name, amount}) =>
<div>{name}: {balanceFormat.format(amount)}</div>;
AccountBalance.propTypes = {
name : PropTypes.string,
amount : PropTypes.number
};
const BalancesView =({checking, savings}) =>
<div>
<AccountBalance {...{name: 'Checking', amount: checking}}/>
<AccountBalance {...{name: 'Savings', amount: savings}}/>
</div>;
BalancesView.propTypes = {
checking : PropTypes.number,
savings : PropTypes.number
};
const toName = account => {
if(account && (typeof account === 'string') && (account.length > 0)){
const name = account.trim();
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
}
return '';
};
const WithdrawView = ({amount, account, onChange, onClick}) => {
const type = 'number';
const value = typeof amount === 'number' ? amount : '';
return (
<div>
<fieldset>
<legend>Withdraw amount from {toName(account)}:</legend>
<input {...{type, value, onChange}} />
</fieldset>
<button {...{onClick}}>Withdraw</button> {/* Listing 10.3 */}
</div>
);
};
WithdrawView.propTypes = {
amount : PropTypes.number,
account : PropTypes.string.isRequired,
onChange : PropTypes.func.isRequired,
onClick : PropTypes.func.isRequired
};
// Container Components
const initialAmount = () => ({amount: undefined});
class WithdrawContainer extends Component {
constructor(props) {
super(props);
this.state = initialAmount(); // local state for local values
}
onChange = ({target: {value}}) => {
const newAmount = parseFloat(value);
const amount =
isNaN(newAmount) || newAmount < 0 ?
initialAmount().amount :
newAmount;
this.setState(prevState => ({...prevState, amount}));
}
withdraw = (store, account, amount) => {
store.dispatch(withdraw({amount, account}));
};
// --- Listing 10.3 Simple banking form with checking text field and withdraw button
handleClick = _event => {
const {
withdraw,
props: {store, account},
state: {amount}
} = this;
const balance = getBalance(account, store.getState());
if (typeof amount !== 'number') {
this.setState(initialAmount);
} else if (balance > amount) {
withdraw(store, account, amount);
} else {
throw new Error('Overdraft error!');
}
};
// --- Listing 10.3 end
render() {
const {handleClick: onClick, onChange, props: {account}, state: {amount}} = this;
return <WithdrawView {...{amount, account, onChange, onClick}} />;
}
}
WithdrawContainer.propTypes = {
account : PropTypes.string.isRequired,
store : storePropType.isRequired
};
class Balances extends Component {
constructor(props) {
super(props);
this.unsubscribe = null;
}
handleChange = () => {
this.forceUpdate(); // i.e. the store is the state
}
componentDidMount() {
const {handleChange, props: {store}} = this;
this.unsubscribe = store.subscribe(handleChange);
}
componentWillUnmount() {
const {unsubscribe} = this;
if(unsubscribe) {
unsubscribe();
this.unsubscribe = null;
}
}
render() {
const {props: {store}} = this;
const accounts = store.getState();
const account = CHECKING;
return (
<React.Fragment>
<BalancesView {...accounts} />
<WithdrawContainer {...{store, account}} />
</React.Fragment>
);
}
}
Balances.propTypes = {
store : storePropType.isRequired
};
// initial state
const accounts = () => ({
[CHECKING]: 100,
[SAVINGS]: 100
});
const store = createStore(updateAccounts, accounts());
ReactDOM.render(
<Balances {...{store}} />,
document.getElementById('root')
);
registerServiceWorker();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment