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;

コメント

タイトルとURLをコピーしました