redux-form で表形式入力的なフォーム

redux-form の FieldArray を使えば、 複数のデータを同時に編集して一括登録、 みたいなことができる。 表形式入力的なフォーム。 DataGrid もどき。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import {
  Field,
  FieldArray,
  reduxForm,
  reducer as formReducer
} from 'redux-form';
import {
  Grid,
  Row,
  Col,
  Button,
  Table,
  FormGroup,
  ControlLabel,
  FormControl
} from 'react-bootstrap';

// bootstrap の CSS を読み込む。
// webpack で CSS Module を使っていることが前提。
import '../node_modules/bootstrap/dist/css/bootstrap.css';

// カスタマーを複数追加するアクション
const ADD_CUSTOMERS = "ADD_CUSTOMERS";

// カスタマーを複数追加するときに使う Action Creator
function addCustomers(customers) {
  return { type: ADD_CUSTOMERS, customers };
}

// 初期状態
const initialState = {
  customers: [
    { code: "10", name: "香川", kana: "カガワ" },
    { code: "4", name: "本田", kana: "ホンダ" },
    { code: "5", name: "長友", kana: "ナガトモ" },
    { code: "9", name: "岡崎", kana: "オカザキ" }
  ]
};

// カスタマーを操作する Reducer
const customersReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_CUSTOMERS:
      return Object.assign({}, state, {
        customers: [...state.customers, ...action.customers]
      });
    default:
      return state;
  }
};

// redux-form が提供する reducer も使う
const rootReducer = combineReducers({
  customers: customersReducer,
  form: formReducer
});

// アプリ唯一のストアを作成
const store = createStore(rootReducer);

// redux-form 用バリデーション関数
// customers の中身を forEach で回して 1 つずつチェックする。
const validate = (values) => {
  const errors = {};
  if (!values.customers || values.customers.length == 0) {
    errors.customers = { _error: "カスタマーを入力してください。" };
  } else {
    const customersArrayErrors = [];
    values.customers.forEach((customer, index) => {
      const customerErros = {};
      if (customer && !customer.code) {
        customerErros.code = "コードを入力してください。";
      }
      if (customer && !customer.name) {
        customerErros.name = "名前を入力してください。";
      }
      if (customer && !customer.kana) {
        customerErros.kana = "カナを入力してください。";
      }
      customersArrayErrors[index] = customerErros;
    });
    if (0 < customersArrayErrors.length) {
      errors.customers = customersArrayErrors;
    }
  }
  return errors;
};

// フォーム入力用コンポーネント
const renderField = ({ input, type, meta: { touched, error } }) => (
  <td>
    <FormControl {...input} type={type}/>
    {touched && error && <span className="text-danger">{error}</span>}
  </td>
);

// カスタマー表形式入力用コンポーネント
// FieldArray の component に渡したコンポーネントには
// props に fileds が渡ってくる。
const renderCustomers = ({ fields, meta: { touched, error } }) => (
  <Table>
    <thead>
      <tr>
        <th>コード</th>
        <th>名前</th>
        <th>カナ</th>
      </tr>
    </thead>
    <tbody>
      {fields.map((customer, index) =>
        <tr key={index}>
          <Field
            name={`${customer}.code`}
            type="text"
            component={renderField}/>
          <Field
            name={`${customer}.name`}
            type="text"
            component={renderField}/>
          <Field
            name={`${customer}.kana`}
            type="text"
            component={renderField}/>
        </tr>
      )}

      {
        touched && error &&
        <tr>
          <td><span className="text-danger">{error}</span></td>
          <td></td>
          <td></td>
        </tr>
      }

      <tr>
        <td>
          <Button type="button" onClick={() => fields.push()}>
            行追加
          </Button>
        </td>
        <td></td>
        <td></td>
      </tr>
    </tbody>
  </Table>
);

// カスタマー登録フォーム本体
class _CustomerTableForm extends Component {
  render() {
    const {
      handleSubmit,
      pristine,
      reset,
      submitting
    } = this.props;

    return (
      <form onSubmit={handleSubmit}>
        <FieldArray name="customers" component={renderCustomers} />

        <FormGroup>
          <Button bsStyle="primary"
                  type="submit"
                  disabled={submitting}>
            保存
          </Button> 
          <Button type="button"
                  disabled={pristine || submitting}
                  onClick={reset}>
            リセット
          </Button>
        </FormGroup>
      </form>
    );
  }
}

// カスタマー登録フォームを redux-form 化。
// form にはユニークな文字列を指定する。
const CustomerTableForm = reduxForm({
  form: "customerForm",
  validate
})(_CustomerTableForm);

// さらに connect でコンテナコンポーネント化。
// redux-form でラップしたコンポーネントには onSubmit の props が
// 生えている。
// submit 時の処理はコンテナコンポーネントで実装する。
const CustomerTableFormContainer = connect(
  state => {
    return {};
  },
  dispatch => {
    return {
      onSubmit: (values) => {
        dispatch(addCustomers(values.customers));
      }
    };
  }
)(CustomerTableForm);

// カスタマー一覧を表示するコンポーネント
class CustomerList extends Component {
  renderCustomers() {
    return this.props.customers.map(c =>
      <tr key={c.code}>
        <td>{c.code}</td>
        <td>{c.name}</td>
        <td>{c.kana}</td>
      </tr>
    );
  }

  render() {
    return (
      <Table>
        <thead>
          <tr>
            <th>コード</th>
            <th>名前</th>
            <th>カナ</th>
          </tr>
        </thead>
        <tbody>
          {this.renderCustomers()}
        </tbody>
      </Table>
    );
  }
}

const CustomerListContainer = connect(
  state => {
    return { customers: state.customers.customers };
  }
)(CustomerList);

class App extends Component {
  render() {
    return (
      <Grid>
        <Row>
          <Col md={12}>
            <CustomerTableFormContainer />
            <CustomerListContainer />
          </Col>
        </Row>
      </Grid>
    );
  }
}

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("app")
);

だんだん、バリデーションのための関数を書くのがメンドクなってきた。