react-redux-form+react-bootstrapで作るフォーム


フロントエンドは初心者同然の僕ですが、ReactとかReduxをいろいろと触ってみました。
bootstrapベースのフォームが作りたかったので`react-redux-form`,`react-bootstrap`を使ってフォーム作ってみました。
とりあえず頑張ったので、フォーム作るまでの手順を書いていこうと思います。

1. 必要なライブラリ等の準備
1. まずはHTMLファイルを用意する 
1. webpackのconfigをそれっぽく用意する
1. nodeのサーバスクリプトを用意する 
1. package.jsonにscriptsの設定をする
1. index.jsを用意する
1. DevToolsの準備
1. configureStoreの作成
1. AppComonentの作成
1. RegistrationFormの作成
1. エラーの場合はラベルや、inputを赤くしたい
1. エラーメッセージの文字を赤くする
1. 最後に

## 必要なライブラリ等の準備

まずは適当なディレクトリを作成しましょう。

```
$ cd ~/
$ mkdir redux-form-sample
```

次に必要なnpmライブラリを導入します。

```
$ npm init

$ npm install -S es6-promise lodash react react-dom redux react-redux react-redux-form react-bootstrap  redux-thunk whatwg-fetch

$ npm install -D babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react babel-preset-react-hmre babel-preset-stage-0 cross-env css-loader cssnext-loader eslint eslint-plugin-babel eslint-plugin-react eventsource-polyfill express extract-text-webpack-plugin path style-loader webpack webpack-dev-middleware webpack-hot-middleware redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
```

## まずはHTMLファイルを用意する

bootstrap用のCSSとかとりあえずcdnにしときます。
至ってシンプルなhtmlですね。

```
<!DOCTYPE html>
<html>
<head>
    <title>Redux form test</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link href="./static/app.css" rel="stylesheet" type="text/css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">

</head>
<body>

<div id="main"></div>

<script src="./static/bundle.js"></script>
</body>
</html>
```

## webpackのconfigをそれっぽく用意する

webpackのconfigファイルを用意します。

``` webpack.config.js
var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

var devFlagPlugin = new webpack.DefinePlugin({
  __DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
});

module.exports = {
  devtool: 'cheap-module-eval-source-map',
  entry: [
    'eventsource-polyfill',
    'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
    './js/index.js'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    publicPath: '/static/',
    filename: 'bundle.js',
    hot: true
  },
  plugins: [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    devFlagPlugin,
    new ExtractTextPlugin('app.css')
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/,
        query: {
          presets: ["react", "es2015", "stage-0"],
          env: {
            "development": {
              "presets": ["react-hmre"]
            }
          }
        }
      },
      { test: /\.css$/, loader: ExtractTextPlugin.extract('css-loader?module!cssnext-loader') }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.json']
  }
};

```

## nodeのサーバスクリプトを用意する

これも適当にいい感じによういします。

``` server.js
var path = require('path');
var webpack = require('webpack');
var express = require('express');
var config = require('./webpack.config');

var app = express();
var compiler = webpack(config);

app.use(require('webpack-dev-middleware')(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath,
  historyApiFallback: true
}));

app.use(require('webpack-hot-middleware')(compiler));

app.get('*', function(req, res) {
  res.sendFile(path.join(__dirname, 'index.html'));
});
  
app.listen(3000, 'localhost', function (err, result) {
  if (err) {
    console.log(err);
  }

  console.log('Listening at localhost:3000');
});

```

## package.jsonにscriptsの設定をする

``` package.json
...
  "scripts": {
    "start": "DEBUG=true node server.js"
  }

```

## index.jsを用意する
一旦適当に`js/index.js`を用意します。

``` js/index.js
import React from 'react'
import ReactDOM from 'react-dom'

```

ここまで準備できたら一旦サーバを起動してみましょう。

```
$ npm start
```

これでブラウザで`localhost:3000`にアクセスしてみて、200が返ってきていたら、大丈夫だと思います。



## DevToolsの準備
ReduxにはStateやActionを可視化するDevToolsがあるみたいです。
その設定をまずしましょう。

詳しいことは[DevToolsのリファレンス](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md)を参照してください。


``` js/containers/DevTools.js

import React from 'react';
import { createDevTools } from 'redux-devtools';

import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';

const DevTools = createDevTools(
    <DockMonitor toggleVisibilityKey='ctrl-h'
                 changePositionKey='ctrl-q'
                 defaultIsVisible={true}>
        <LogMonitor theme='solarized' />
    </DockMonitor>
);

export default DevTools;

```

## configureStoreの作成

createFormsを使います。これで`react-redux-form`のreducerが準備できます。
react-redux-formでは`redux-thunk`が必要なので、middlewareの登録もします。
今回はDevToolsも使うので`DevTools.instrument()`も実行しています。


``` js/store/configureStore.js
import {createStore, applyMiddleware, combineReducers, compose} from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createForms } from 'react-redux-form';

import {createStore, applyMiddleware, combineReducers, compose} from 'redux';
import thunkMiddleware from 'redux-thunk';
import { combineForms } from 'react-redux-form';

let createStoreWithMiddleware = compose(
  applyMiddleware(thunkMiddleware),
  DevTools.instrument()
)(createStore);

let reducers = combineForms({
    user: {
      firstName: "",
      lastName: "",
      email: ""
    }
});


export default function configureStore(initialState) {
  return createStoreWithMiddleware(reducers, initialState);
}

```

もし他のreducerも使いたい場合は以下のように`createForms`を使用するのが良さそうです。

``` js/store/configreStore.js
import { combineForms } from 'react-redux-form';

const reducers = combineReducers({
  otherReducer,
  ...createForms({
    giver: giver
  })
});
```



## AppComonentの作成

この辺はReduxのチュートリアルなんかでも出て来るのと同じ感じで実装します。
このタイミングで`<DevTools />`を記載してあげましょう。


``` js/containers/App.js
import React from 'react';
import {Provider} from 'react-redux';
import configureStore from '../store/configureStore';
import DevTools from '../containers/DevTools';

const store = configureStore();

export default React.createClass({
  render() {
    return (
      <Provider store={store}>
        <div>
          <DevTools />
        </div>
      </Provider>
    );
  }
});
```

Appコンポーネントが作成できたら`js/index.js`に以下の記述を追加します。

``` js/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './containers/App'

ReactDOM.render(<App />, document.getElementById('main'));

```
ここまで出来たらブラウザでアクセスしてみてください。

![kobito.1475812785.189749.png](https://qiita-image-store.s3.amazonaws.com/0/2095/453ee6cb-cdc9-3f43-7ca9-d7092c18d9df.png "kobito.1475812785.189749.png")

こんな感じにDevToolsが表示されるかと思います。


## RegistrationFormの作成
ここまで出来たらあとはさくっと入力フォームを作りましょう。
まずは`RegistrationForm.js `を実装します。

バリデーションの指定は`validators`で関数を指定する形にしました。今回は簡易的な実装です。
Errorコンポーネントでバリデーションがfalseの場合エラーメッセージが表示されるようになっています。
`Control`,`Error`のヒモ付はmodel属性で行っています。
詳しいことは[react-redux-formのバリデーションのドキュメント](https://davidkpiano.github.io/react-redux-form/docs/guides/validation.html)を参照してください。


``` js/containers/RegistrationForm.js

import React, { Component } from 'react'
import { connect } from 'react-redux';
import { Control, Field, Form, actions, Errors } from 'react-redux-form';
import {FormGroup, ControlLabel, Button} from 'react-bootstrap'

const isRequired = (val) => !!(val && val.length);

class RegistrationForm extends Component {
  handleSubmit(user) {
    const { dispatch } = this.props;
    console.log(user);

    // TODO 適当に頑張る
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="col-sm-6 col-sm-offset-2">
            <Form model="user" onSubmit={(user) => this.handleSubmit(user)}>
              <FormGroup controlId="name" >
                <ControlLabel>firstName</ControlLabel>
                <Control.text
                  model="user.name"
                  className="form-control"
                  validators={{isRequired}}
                  validateOn="blur" />
                <Errors
                  model="user.name"
                  messages={{
                      isRequired: '入力必須項目です！'
                  }}
                />
              </FormGroup>
              <div className="col-sm-4 col-md-offset-4">
                <button type="submit" href="#" className="btn btn-primary btn-lg btn-block">登録する</button>
              </div>
            </Form>
          </div>
        </div>
      </div>
    );
  }
}

const selector = (state) => ({ user: state.user });
export default connect(selector)(RegistrationForm);

```

### つぎにApp.jsに先程のRegistrationFormを追加しましょう。

``` js/containers/test.rb

import React from 'react';
import {Provider} from 'react-redux';
import configureStore from '../store/configureStore';
import DevTools from '../containers/DevTools';

++ import RegistrationForm from '../containers/RegistrationForm'

const store = configureStore();

export default React.createClass({
  render() {
    return (
      <Provider store={store}>
        <div>
++          <RegistrationForm />
          <DevTools />
        </div>
      </Provider>
    );
  }
});

```

## エラーの場合はラベルや、inputを赤くしたい

bootstrap使っているとlabelやエラーメッセージを赤くしたい。
でも現状だと色つかないんですよね。

![kobito.1475815149.464789.png](https://qiita-image-store.s3.amazonaws.com/0/2095/16966eb3-aa90-9a69-cf1a-20289402a5bb.png "kobito.1475815149.464789.png")


react-bootstrapでは`FormGroup`に対して`validationState`の属性で色を追加できるようです。

```
<FormGroup validationState="error">...</FormGroup>
```

しかし、FormGroupでstate触ることできない・・・困った。。。

悩んだ結果、FormGroupをラップするものを作り事にしました。
本当はもっと良い方法あるのかもしれないけど、思いつかないので・・・。

だれか、よい方法を見つけたら教えてください。
とりあえず僕がやった実装はこんな感じです。

``` js/containers/CustomFormGroup.js

import React from 'react';
import { FormGroup } from 'react-bootstrap';
import { connect } from 'react-redux';
import { getModel } from 'react-redux-form';

const ExtFormGroup = ({children, bsClass, bsSize, controlId, validationState}) => {
    let props = {
        bsClass,
        bsSize,
        controlId,
        validationState,
    }
    return (
        <FormGroup {...props} >{children}</FormGroup>
    );
}

const mapStateToProps = (state, { model, bsClass, bsSize, controlId, validationState, children }) => {
    const modelString = getModel(model, state);

    if (modelString != undefined) {
        const formValue = getModel(state.forms, modelString);
        if (formValue !== undefined && !formValue.valid) {
            validationState="error";
        }
    }

    return {
        children,
        bsClass,
        bsSize,
        controlId,
        validationState
    }
}

const CustomFormGroup = connect(
    mapStateToProps
)(ExtFormGroup);

export default CustomFormGroup;

```

あとはFormGroupをCustomFormGroupに書き換えればエラーが発生した場合にラベルやらinputが赤くなります。

``` js/containers/RegistrationForm.js

import React, { Component } from 'react'
import { connect } from 'react-redux';
import { Control, Field, Form, actions, Errors } from 'react-redux-form';
++ import {FormGroup, ControlLabel, Button} from 'react-bootstrap'

import CustomFormGroup from './CustomFormGroup'

const isRequired = (val) => !!(val && val.length);

class RegistrationForm extends Component {
  handleSubmit(user) {
    const { dispatch } = this.props;
    console.log(user);

    // TODO 適当に頑張る
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="col-sm-6 col-sm-offset-2">
            <Form model="user" onSubmit={(user) => this.handleSubmit(user)}>
--				  <FormGroup controlId="name">            
++              <CustomFormGroup controlId="name" model="user.name">
                <ControlLabel>firstName</ControlLabel>
                <Control.text
                  model="user.name"
                  className="form-control"
                  validators={{isRequired}}
                  validateOn="blur" />
                <Errors
                  model="user.name"
                  messages={{
                      isRequired: '入力必須項目です！'
                  }}
                />
++              </CustomFormGroup>
--				</FormGroup>
              <div className="col-sm-4 col-md-offset-4">
                <button type="submit" href="#" className="btn btn-primary btn-lg btn-block">登録する</button>
              </div>
            </Form>
          </div>
        </div>
      </div>
    );
  }
}

const selector = (state) => ({ user: state.user });
export default connect(selector)(RegistrationForm);

```

これでエラーが発生するとこんな感じになります。
![kobito.1475815534.076141.png](https://qiita-image-store.s3.amazonaws.com/0/2095/837453fa-63cb-52b7-41f2-4bbb35974e45.png "kobito.1475815534.076141.png")


## エラーメッセージを赤くする
最後にエラーメッセージだけ赤くならなかったのですが、これはErrorコンポーネントだけで対応できます。

```
<Errors
  model="user.name"
  messages={{
      isRequired: '入力必須項目です！'
  }}
  component={(props) => <span className="help-block">{props.children}</span>}
/>
```

こんな感じに`component`を指定すればいいみたいです。


## 最後に
最近のフロントエンド難しすぎ・・・。
今回書いたコードはgithubで公開しておきました。
間違っていたらPull Requestください。

[polidog/react-redux-form-sample1](https://github.com/polidog/react-redux-form-sample1)