
React練習 – 割り勘計算アプリ(4)
前回の投稿では、ヘッダの支払額入力に反応して割り勘計算を行い、結果をヘッダおよび各明細に反映させるように実装を行った。
今回は、各明細の操作を可能にする。まずは、明細コンポーネントの各入力要素のchangeイベントにハンドラメソッドを紐付ける。
class Detail extends Component {
render () {
return (
<div>
<input
type="text"
value={this.props.name}
onChange={e => this.handleNameChange(e)}
/>
<label>
<input
type="radio"
value="fix"
checked={this.props.kind === 'fix'}
onChange={e => this.handleKindChange(e)}
/>固定
</label>
<label>
<input
type="radio"
value="split"
checked={this.props.kind === 'split'}
onChange={e => this.handleKindChange(e)}
/>割り勘
</label>
<input
type="text"
value={this.props.amount}
onChange={e => this.handleAmountChange(e)}
/>
</div>
);
}
各ハンドラメソッドは、親コンポーネントから渡されpropsに格納されているコールバックを呼び出して処理を委譲する。
handleNameChange (e) {
this.props.onNameChange(this.props.rowIndex, e.target.value);
}
handleKindChange (e) {
this.props.onKindChange(this.props.rowIndex, e.target.value);
}
handleAmountChange (e) {
const amount = Number(e.target.value);
if (amount) {
this.props.onAmountChange(this.props.rowIndex, amount);
}
}
親コンポーネント(BillSplitter)側は、各種イベントハンドリングの際に割り勘計算・状態更新を行う処理を、再利用可能なようにまずは別メソッドに切り出した。
recalcSplitAmount (payment, participants) {
// (支払額 - 固定支払いの人の合計) / (割り勘支払い人数) => 一人あたり割り勘金額
const fixSum = participants.filter(p => p.kind === 'fix').map(p => p.amount).reduce((a, x) => a += x, 0);
const splitNum = participants.filter(p => p.kind === 'split').length;
let splitAmount;
if (payment > fixSum && splitNum) {
// 100円単位で切り捨て
const splitAmountTmp = (payment - fixSum) / splitNum;
splitAmount = Math.floor(splitAmountTmp / 100) * 100;
} else {
splitAmount = 0;
}
const newParticipants = participants.map(p => {
const amount = p.kind === 'fix' ? p.amount : splitAmount;
return Object.assign({}, p, {amount: amount});
});
const total = newParticipants.map(p => p.amount).reduce((a, x) => a += x, 0);
const remainder = payment - total;
// 状態更新
this.setState({
payment: payment,
total: total,
remainder: remainder,
participants: newParticipants,
});
}
そして子コンポーネント(Detail)から発生した各イベントのハンドラメソッドを定義。
handleParticipantNameChange (index, newName) {
const participants = this.state.participants.map((p, i) => {
const name = (i === index) ? newName : p.name;
return Object.assign({}, p, {name: name});
})
// 金額の変更はないので、参加者のデータ更新のみ
this.setState({
participants: participants,
});
}
handleParticipantKindChange (index, newKind) {
const participants = this.state.participants.map((p, i) => {
const kind = (i === index) ? newKind : p.kind;
return Object.assign({}, p, {kind: kind});
})
this.recalcSplitAmount(this.state.payment, participants);
}
handleParticipantAmountChange (index, newAmount) {
const participants = this.state.participants.map((p, i) => {
const amount = (i === index) ? newAmount : p.amount;
return Object.assign({}, p, {amount: amount});
})
this.recalcSplitAmount(this.state.payment, participants);
}
参加者の名前変更時には金額は変わらないので、setStateで渡すオブジェクトにはparticipantsのみを定義している(7-9行目)。
setStateはstateをマージしてくれるので、変更のあったプロパティのみ渡してあげればよい。
あとはrenderメソッドにてイベントハンドリングのコールバックを指定してやればOK。
<Detail
key={idx}
rowIndex={idx}
{...person}
onNameChange={(i, n) => this.handleParticipantNameChange(i, n)}
onKindChange={(i, k) => this.handleParticipantKindChange(i, k)}
onAmountChange={(i, a) => this.handleParticipantAmountChange(i, a)}
/>

次回は、人数追加を実装する予定。
現時点でのソースコード(App.js)は以下のとおり。
import React, {Component} from 'react';
import './App.css';
// ヘッダーコンポーネント
class Header extends Component {
render () {
return (
<div>
<div>
<label>支払額:
<input type="text"
value={this.props.payment}
onChange={e => this.handlePaymentChange(e)} />
</label>
</div>
<div>
<label>合計額:<span>{this.props.total}</span></label>
</div>
<div>
<label>端数:<span>{this.props.remainder}</span></label>
</div>
<div>
<label>人数:
<input type="text" value={this.props.participantsNumber} />
</label>
</div>
</div>
);
}
handlePaymentChange (e) {
const payment = Number(e.target.value);
if (payment) {
this.props.onChange(payment);
}
}
}
// 明細コンポーネント
class Detail extends Component {
render () {
return (
<div>
<input
type="text"
value={this.props.name}
onChange={e => this.handleNameChange(e)}
/>
<label>
<input
type="radio"
value="fix"
checked={this.props.kind === 'fix'}
onChange={e => this.handleKindChange(e)}
/>固定
</label>
<label>
<input
type="radio"
value="split"
checked={this.props.kind === 'split'}
onChange={e => this.handleKindChange(e)}
/>割り勘
</label>
<input
type="text"
value={this.props.amount}
onChange={e => this.handleAmountChange(e)}
/>
</div>
);
}
handleNameChange (e) {
this.props.onNameChange(this.props.rowIndex, e.target.value);
}
handleKindChange (e) {
this.props.onKindChange(this.props.rowIndex, e.target.value);
}
handleAmountChange (e) {
const amount = Number(e.target.value);
if (amount) {
this.props.onAmountChange(this.props.rowIndex, amount);
}
}
}
// 割り勘計算機コンポーネント
class BillSplitter extends Component {
constructor(props) {
super(props);
this.state = {
payment: 0, //支払額
total: 0, //合計額
remainder: 0, //端数
participants: new Array(3).fill().map(() => ({name: '', kind: 'split', amount: 0})), //参加者
};
}
handlePaymentChange (payment) {
this.recalcSplitAmount(payment, this.state.participants);
}
recalcSplitAmount (payment, participants) {
// (支払額 - 固定支払いの人の合計) / (割り勘支払い人数) => 一人あたり割り勘金額
const fixSum = participants.filter(p => p.kind === 'fix').map(p => p.amount).reduce((a, x) => a += x, 0);
const splitNum = participants.filter(p => p.kind === 'split').length;
let splitAmount;
if (payment > fixSum && splitNum) {
// 100円単位で切り捨て
const splitAmountTmp = (payment - fixSum) / splitNum;
splitAmount = Math.floor(splitAmountTmp / 100) * 100;
} else {
splitAmount = 0;
}
const newParticipants = participants.map(p => {
const amount = p.kind === 'fix' ? p.amount : splitAmount;
return Object.assign({}, p, {amount: amount});
});
const total = newParticipants.map(p => p.amount).reduce((a, x) => a += x, 0);
const remainder = payment - total;
// 状態更新
this.setState({
payment: payment,
total: total,
remainder: remainder,
participants: newParticipants,
});
}
handleParticipantNameChange (index, newName) {
const participants = this.state.participants.map((p, i) => {
const name = (i === index) ? newName : p.name;
return Object.assign({}, p, {name: name});
})
// 金額の変更はないので、参加者のデータ更新のみ
this.setState({
participants: participants,
});
}
handleParticipantKindChange (index, newKind) {
const participants = this.state.participants.map((p, i) => {
const kind = (i === index) ? newKind : p.kind;
return Object.assign({}, p, {kind: kind});
})
this.recalcSplitAmount(this.state.payment, participants);
}
handleParticipantAmountChange (index, newAmount) {
const participants = this.state.participants.map((p, i) => {
const amount = (i === index) ? newAmount : p.amount;
return Object.assign({}, p, {amount: amount});
})
this.recalcSplitAmount(this.state.payment, participants);
}
render () {
const details = this.state.participants.map((person, idx) => {
return (
<Detail
key={idx}
rowIndex={idx}
{...person}
onNameChange={(i, n) => this.handleParticipantNameChange(i, n)}
onKindChange={(i, k) => this.handleParticipantKindChange(i, k)}
onAmountChange={(i, a) => this.handleParticipantAmountChange(i, a)}
/>
);
})
return (
<>
<Header
payment={this.state.payment}
total={this.state.total}
remainder={this.state.remainder}
participantsNumber={this.state.participants.length}
onChange={(p) => this.handlePaymentChange(p)}
/>
{details}
</>
);
}
}
function App () {
return (
<div className="App">
<BillSplitter />
</div>
);
}
export default App;