React練習 – 割り勘計算アプリ(5)

サンプル開発

前回の投稿までで大部分の実装ができたので、残りは人数の増減アクションの追加。各明細の横に削除と追加のボタンを配置して、クリック時のイベントで処理を行う。

明細のJSXに、以下のようにボタンを配置してclickのイベントハンドラ呼び出しを記述。

        <input
          type="button"
          value="削除"
          disabled={!this.props.deletable}
          onClick={e => this.handleDeleteClick(e)}
        />
        <input
          type="button"
          value="追加"
          onClick={e => this.handleAddClick(e)}
        />

それぞれのイベントハンドラは、親コンポーネントへ処理を委譲。

  handleDeleteClick (e) {
    this.props.onDeleteClick(this.props.rowIndex);
  }

  handleAddClick (e) {
    this.props.onAddClick(this.props.rowIndex);
  }

親コンポーネントのイベントハンドラでは、stateに保持するparticipants配列を対象に要素の削除または追加を行う。直接配列を更新するのではなく、新しい配列を作成する。

  handleParticipantDeleteClick (index) {
    const p1 = this.state.participants.slice(0, index)
    const p2 = this.state.participants.slice(index + 1);
    const participants = p1.concat(p2);
    this.recalcSplitAmount(this.state.payment, participants);
  }

  handleParticipantAddClick (index) {
    const p1 = this.state.participants.slice(0, index + 1)
    const p2 = this.state.participants.slice(index + 1);
    const participants = p1.concat({name: '', kind: 'split', amount: 0}).concat(p2);
    this.recalcSplitAmount(this.state.payment, participants);
  }

BillSplitterのJSXで明細コンポーネントを定義する際、イベントハンドラをプロパティ渡ししている(9-10行目)。また、明細行が2行より少なくなってしまうともはや割り勘ではなくぼっち飯なので、削除ボタンの非活性制御を5行目で指定している。これももちろんstateの状態変更に連動して動的に制御が切り替わる。

        <Detail
          key={idx}
          rowIndex={idx}
          {...person}
          deletable={this.state.participants.length > 2}
          onNameChange={(i, n) => this.handleParticipantNameChange(i, n)}
          onKindChange={(i, k) => this.handleParticipantKindChange(i, k)}
          onAmountChange={(i, a) => this.handleParticipantAmountChange(i, a)}
          onDeleteClick={(i) => this.handleParticipantDeleteClick(i)}
          onAddClick={(i) => this.handleParticipantAddClick(i)}
        />

以上で実装が完了した。

最終的な画面。

ソースは以下の通り。

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>合計額:
            <input type="text"
              value={this.props.total} readOnly />
          </label>
        </div>
        <div>
          <label>端数:
            <input type="text"
              value={this.props.remainder} readOnly />
          </label>
        </div>
        <div>
          <label>人数:
            <input type="text"
              value={this.props.participantsNumber} readOnly />
          </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)}
        />
        <input
          type="button"
          value="削除"
          disabled={!this.props.deletable}
          onClick={e => this.handleDeleteClick(e)}
        />
        <input
          type="button"
          value="追加"
          onClick={e => this.handleAddClick(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);
    }
  }

  handleDeleteClick (e) {
    this.props.onDeleteClick(this.props.rowIndex);
  }

  handleAddClick (e) {
    this.props.onAddClick(this.props.rowIndex);
  }

}

// 割り勘計算機コンポーネント
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);
  }

  handleParticipantDeleteClick (index) {
    const p1 = this.state.participants.slice(0, index)
    const p2 = this.state.participants.slice(index + 1);
    const participants = p1.concat(p2);
    this.recalcSplitAmount(this.state.payment, participants);
  }

  handleParticipantAddClick (index) {
    const p1 = this.state.participants.slice(0, index + 1)
    const p2 = this.state.participants.slice(index + 1);
    const participants = p1.concat({name: '', kind: 'split', amount: 0}).concat(p2);
    this.recalcSplitAmount(this.state.payment, participants);
  }

  render () {
    const details = this.state.participants.map((person, idx) => {
      return (
        <Detail
          key={idx}
          rowIndex={idx}
          {...person}
          deletable={this.state.participants.length > 2}
          onNameChange={(i, n) => this.handleParticipantNameChange(i, n)}
          onKindChange={(i, k) => this.handleParticipantKindChange(i, k)}
          onAmountChange={(i, a) => this.handleParticipantAmountChange(i, a)}
          onDeleteClick={(i) => this.handleParticipantDeleteClick(i)}
          onAddClick={(i) => this.handleParticipantAddClick(i)}
        />
      );
    })
    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をコピーしました