React UI library の antd について (1) - Button
React UI library の antd について (2) - Layout
React UI library の antd について (3) - redux-form
React のUI libraryであるantdは使い方はとても簡単でcomponentsも種類が多いのを見てきました。Layout componentも比較的簡単に使えました。
今回はFormとInput componentを見ていきます。
validateの方法は2つ示唆されています。最初はantd Formで標準のForm.createを使うことです。2番目はForm.createを使わずに行うことです。
2番目の方法としては、まったく手動で行うとかいろいろあるでしょうが、今回は**redux-form**を利用します。
Form.createの使い勝手は少し複雑な気がします。比べてredux-formを使う方法はシンプルでわかりやすいです。
Form.createを使った場合 - Antd標準
antdのForm component とInput componentの使い方を見ていきます。ここではform の validateは、antd Formの標準のやり方ですが、Form.createでdecorateすること行います。
以下のコードは、HorizontalLoginForm という component を Form.create で decorate しています。
const WrappedHorizontalLoginForm =
Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
Form.create で decorate された component には this.props.form property が渡されます。this.props.formはいくつかの属性を持ちます。以下にその中のいくつかを示します。(==> 全リストはこちら)
- validateFields
- getFieldDecorator
- getFieldError
- getFieldsError
- isFieldTouched
<Form.Item />の中では、getFieldDecoratorでinput controlをラップします。その中でvalidate ruleをまとめて記述していくことになります。一か所で全ての情報をかけるので、ある意味見やすいと言えます。
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import { Form, Icon, Input, Button } from 'antd';
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class HorizontalLoginForm extends React.Component {
componentDidMount() {
// To disabled submit button at the beginning.
this.props.form.validateFields();
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
// Only show error after a field is touched.
const usernameError = isFieldTouched('username') && getFieldError('username');
const passwordError = isFieldTouched('password') && getFieldError('password');
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}>
Log in
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
ReactDOM.render(<WrappedHorizontalLoginForm />, document.getElementById('root'));
redux-form を使った場合
【注意】(2019/07/13)
いろいろ試した結果、redux-formとreact-router v4の組み合わせがうまく動作しないという結論に達しました。reduxForm()とconnect()という2つのHOCで2重にラップした時に、propsが、最終的なコンポーネントにうまく伝わっていかない感じです。ラップする順番を変えてもだめでした。しかしreduxForm()の代わりに、antdのForm.create()でラップすればうまくいきました。何か私の見落としがあるのかもしれませんが、当面はForm.create()でいろいろ試していきたいと思います。
Form component
マニュアルには次のような記述があります。Form.create はformデータを集めvalidateします。しかしForm.createによるやり方が気に入らない場合は、使う必要はありません。手動でvalidateを行っても構いません。
ここではvalidateをredux-formで行いたいと思います。個人的にはこちらの方がシンプルで柔軟なような気がします。
redux-form (1) - Simple Form Example
つまりantdとredux-formを組み合わせて使うわけです。
必要なライブラリをインストールします。
yarn add redux react-redux redux-form
以下はantdとredux-formを同時に使用しているコードです。Form.createは使わず、代わりにreduxFormを使っていることに注意してください。
- <Form />はantdです。
- <Field />はredux-formです。
<Field /> の component={renderInput} に注目してください。renderInput は antd のcomponentで構成されたcomponentになります。renderInput には prefix等のpropを引き渡しています。
import React from 'react';
import 'antd/dist/antd.css';
import { Form, Icon, Button } from 'antd';
import { Field, reduxForm } from 'redux-form';
import renderInput from './input';
const HorizontalLoginForm = props => {
const { handleSubmit, pristine, submitting, message } = props
return (
<Form layout="inline" onSubmit={handleSubmit}>
<Field
hasFeedback
type="text"
name="name"
component={renderInput}
disabled={submitting}
label="Name"
placeholder="Full Name"
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
/>
<Field
hasFeedback
type="password"
name="password"
component={renderInput}
disabled={submitting}
label="Password"
placeholder="Password"
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
/>
<Form.Item>
<Button type="primary" htmlType="submit" className="btn-submit" disabled={pristine || submitting}>
Log in
</Button>
</Form.Item>
{!!message && <p className="caption-invalid">{message}</p>}
</Form>
);
}
const validate = values => {
const errors = {};
if (!values.name) {
errors.name = "Name can't be blank";
}
if (!values.password) {
errors.password = "Password can't be blank";
}
return errors;
};
export default reduxForm({
form: 'login-form',
validate,
})(HorizontalLoginForm);
以下がrenderInput componentのソースです。propsを展開して、inputやrestを取り出していることに注目してください。そのままInput componentに展開されます。特に**(1)input object (redux-form) はvalueを含んでおり、(2)Input (antd component) のvalue属性の値としてそのまま適用されます。このvalueはactionによりRedux stateにbindされています。**またrestにはprefixが含まれており、そのままアイコン情報が展開されます。
(1) Redux-form Fieldのinput object
(2) Antd Input component
import React from 'react';
import { Form, Input } from 'antd';
const newComponent = props => {
const { input, meta, hasFeedback, label, ...rest } = props;
const hasError = meta.touched && meta.invalid;
return (
<Form.Item
label={label}
help={hasError && meta.error}
hasFeedback={hasFeedback && hasError}
validateStatus={hasError ? 'error' : 'success'}
>
<Input {...input} {...rest} />
</Form.Item>
);
};
export default newComponent;
ここで分割代入 (Destructuring assignment) 構文を思い出しましょう。
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a); // 10
console.log(b); // 20
console.log(rest); // {c: 30, d: 40}
以下のindex.jsでは主にredux-formの設定を行っています。Redux storeの設定や、onSubmit関数の定義を行っています。
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import { reducer as reduxFormReducer } from 'redux-form'
import HorizontalLoginForm from './HorizontalLoginForm'
const dest = document.getElementById('root')
const reducer = combineReducers({
form: reduxFormReducer // mounted under "form"
})
const store = createStore(reducer)
const showResults = values =>
new Promise(resolve => {
setTimeout(() => {
// simulate server latency
window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`)
resolve()
}, 500)
})
let render = () => {
// const HorizontalLoginForm = require('./HorizontalLoginForm').default
ReactDOM.hydrate(
<Provider store={store}>
<h2>Form</h2>
<HorizontalLoginForm onSubmit={showResults} />
</Provider>,
dest
)
}
render()
今回は以上です。