Skip to content

Instantly share code, notes, and snippets.

@peerreynders
Created April 21, 2018 18:46
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/e02df86179e83b42eb23a1ea68ec78bb to your computer and use it in GitHub Desktop.
Save peerreynders/e02df86179e83b42eb23a1ea68ec78bb to your computer and use it in GitHub Desktop.
RxJS in Action Ch10 3B: 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
// Listing:
// 10.3 Simple banking form with checking text field and withdraw button (p.289)
//
// Variation B: Use Top-level source of truth instead of redux
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import registerServiceWorker from './registerServiceWorker';
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 = (action, state) => { // reversed arguments
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
};
};
// 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}));
}
// --- Listing 10.3 Simple banking form with checking text field and withdraw button
handleClick = _event => {
const {
props: {withdraw, balance},
state: {amount}
} = this;
if (typeof amount !== 'number') {
this.setState(initialAmount);
} else if (balance > amount) {
withdraw(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,
balance : PropTypes.number.isRequired,
withdraw : PropTypes.func.isRequired
};
class Balances extends Component {
constructor(props) {
super(props);
this.state = accounts(); // top-level source of truth
}
send(action) { // use reducer
this.setState(prevState => updateAccounts(action, prevState));
}
makeWithdraw = account =>
amount => { // use action creator
this.send(withdraw({account, amount}));
};
render() {
const {makeWithdraw, state: accounts} = this;
const account = CHECKING;
const balance = getBalance(account, accounts)
const withdraw = makeWithdraw(account);
return (
<React.Fragment>
<BalancesView {...accounts} />
<WithdrawContainer {...{balance, account, withdraw}} />
</React.Fragment>
);
}
};
// initial state
const accounts = () => ({
[CHECKING]: 100,
[SAVINGS]: 100
});
ReactDOM.render(
<Balances />,
document.getElementById('root')
);
registerServiceWorker();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment