Inline Form in HTML Table with React


現在 react の勉強がてら、昔作った rails アプリのSPR化を行っている。

Bootstrap on erb では簡単にできていた inline for in Table の実装に以外に手こずったのでまとめておく。


Base Form

import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class BaseForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
  // this.props.model.attrs を 初期state として持つ
componentWillMount(){
const model = this.props.model;
Object.keys(model).map((attr) => {
let initState = {};
initState[attr] = model[attr];
this.setState(initState);
});
}
  // 下位componentでの変化を受け、stateに変更を加える
handleChange(name, e) {
let newState = {};
newState[name] = e.target.value;
this.setState(newState);
}
  handleSubmit(e) {
e.preventDefault();
this.props.onSubmit(this.state);
}
  render() {
const newChildren = React.Children.map(
this.props.children,
(child) => {
      switch (typeof child) {
// React.cloneElementはtextnodeを受け取らない
case 'string':
return child;
      // React要素だった場合は 初期値とcb を渡す
case 'object':
const newProps = {
onChange: this.handleChange,
value: this.state ?
this.state[child.props.name] || this.props.model[child.props.name] :
this.props.model[child.props.name]
};
return (
<div>
<label>{child.props.name}</label><br />
{React.cloneElement(child, newProps)}
</div>
);
      default:
return null;
}
});
    return (
<form onSubmit={this.handleSubmit}>
{ newChildren }
<button type="submit" className="btn btn-primary">Submit</button>
</form>
);
}
}
BaseForm.propTypes = {
model: PropTypes.object,
onSubmit: PropTypes.func
}

Table Inline Form

import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class TableInlineForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
  // this.props.model.attrs を 初期state として持つ
componentWillMount(){
const model = this.props.model;
Object.keys(model).map((attr) => {
let initState = {};
initState[attr] = model[attr];
this.setState(initState);
});
}
  // 下位componentでの変化を受け、stateに変更を加える
handleChange(name, e) {
let newState = {};
newState[name] = e.target.value;
this.setState(newState);
}
  handleSubmit(e) {
e.preventDefault();
this.props.onSubmit(this.state);
}
  render() {
const newChildren = React.Children.map(
this.props.children,
(child) => {
      switch (typeof child) {
// React.cloneElementはtextnodeを受け取らない
case 'string':
return child;
      // React要素だった場合は 初期値とcb を渡す
case 'object':
const newProps = {
form: `form${this.props.formId}`,
onChange: this.handleChange,
value: this.state ?
this.state[child.props.name] || this.props.model[child.props.name] :
this.props.model[child.props.name]
};
        return (
<td>
{React.cloneElement(child, newProps)}
</td>
);
      default:
return null;
}
});
    return (
<tr>
<td><form id={`form${this.props.formId}`} onSubmit={this.handleSubmit} >{this.props.model.id}</form></td>
{ newChildren }
<td><button form={`form${this.props.formId}`} type='submit' className='btn btn-primary'>Submit</button></td>
</tr>
);
}
}
TableInlineForm.propTypes = {
formId: PropTypes.string,
model: PropTypes.object,
onSubmit: PropTypes.func
}

変更があったのは render() メソッドのみ。

本来 <table> 下に <form> を置くことは出来ないが、HTML5 の form attribute を props として <input> に付与することで実現できた。

BaseForm 自体の実装はこれを参考にしている。

React を書いていると生の HTML(実際には JSX ) を書く機会が増えるので、なんというか Web ページを作っている感があって楽しい。なにより component を一つ仕上げたときの全能感がすごく爽快で気持ちが良い。

よしなに