dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Material-UIでrefを使う

メモ。
以下のようなコンポーネント<input type="text"> 項目を Material-UI の TextField に置き換えようと思ったが
ref で参照しているのをそのまま TextField でも使えるのか迷った。

import React, { PropTypes } from 'react';

export default class AddTodoForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const node = this.refs.input;
    const text = node.value.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    node.value = '';
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type="text" ref="input" />
          <button type="submit">
            Add Todo
          </button>
        </form>
      </div>
    );
  }
}

AddTodoForm.propTypes = {
  onSubmit: PropTypes.func.isRequired,
};


先に結論

値の参照だけしたいときは

this.refs.xxx.getValue()

とし、DOM Node を取得する場合は

this.refs.xxx.getInputNode()

を使う。

両方ともドキュメントには載っておらず、前者は以下の Stack Overflow から
javascript - How get data from material-ui TextField, DropDownMenu components? - Stack Overflow

後者は以下のようにブラウザの開発者コンソールから無理やり見つけた。

f:id:dackdive:20161123195515p:plain


string ref を使う場合

 import React, { PropTypes } from 'react';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';

 export default class AddTodoForm extends React.Component {
   handleSubmit(e) {
     e.preventDefault();
-    const node = this.refs.input;
+    const node = this.refs.input.getInputNode();
     const text = node.value.trim();
     if (!text) {
       return;
@@ -16,10 +18,8 @@ export default class AddTodoForm extends React.Component {
     return (
       <div>
         <form onSubmit={(e) => this.handleSubmit(e)}>
-          <input ref="input" />
-          <button type="submit">
-            Add Todo
-          </button>
+          <TextField id="todo-title" ref="input" />
+          <RaisedButton label="Add Todo" onTouchTap={(e) => this.handleSubmit(e)} />
         </form>
       </div>
     );
diff --git a/src/containers/App.js b/src/containers/App.js
index 8068f95..4ee5acf 100644
--- a/src/containers/App.js
+++ b/src/containers/App.js
@@ -18,4 +18,3 @@ export default class App extends React.Component {
     );
   }
 }


arrow function による ref を使う場合

上述した string ref は現在推奨されていないらしい
(参考:http://reactjs.cn/react/docs/more-about-refs.html#the-ref-string-attribute
ので、arrow 関数を使った場合の書き方も載せておく。

https://facebook.github.io/react/docs/refs-and-the-dom.html

 import React, { PropTypes } from 'react';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';

 export default class AddTodoForm extends React.Component {
   handleSubmit(e) {
     e.preventDefault();
-    const node = this.refs.input;
+    const node = this.input.getInputNode();
     const text = node.value.trim();
     if (!text) {
       return;
@@ -16,10 +18,8 @@ export default class AddTodoForm extends React.Component {
     return (
       <div>
         <form onSubmit={(e) => this.handleSubmit(e)}>
-          <input ref="input" />
-          <button type="submit">
-            Add Todo
-          </button>
+          <TextField id="todo-title" ref={(input) => this.input = input} />
+          <RaisedButton label="Add Todo" onTouchTap={(e) => this.handleSubmit(e)} />
         </form>
       </div>
     );
     );
   }
 }

API Blueprintによるドキュメント開発環境【2016年冬】

ここから少しアップデートしたのでメモ。

リポジトリ


特徴

markdown -> html への変換には aglio、ローカルサーバーは --server オプションを使う

ローカルで markdown ファイルを html に変換するのに aglio を使うところは以前と同じ。
改めて調べてみると、--server というオプションでライブリロード機能つきのローカルサーバーの起動までやってくれるらしく、browserSync は不要だった。

また、ポート番号については README に記載されていないが、-p オプションで指定できた。
参考:Support for different port? · Issue #12 · danielgtaylor/aglio

$ ./node_modules/aglio/bin/aglio.js
Usage: node_modules/aglio/bin/aglio.js [options] -i infile [-o outfile -s]

オプション:
  -i, --input           Input file
  -o, --output          Output file
  -t, --theme           Theme name or layout file        [デフォルト: "default"]
  -f, --filter          Sanitize input from Windows    [真偽] [デフォルト: true]
  -s, --server          Start a local live preview server
  -h, --host            Address to bind local preview server to
                                                       [デフォルト: "127.0.0.1"]
  -p, --port            Port for local preview server         [デフォルト: 3000]
  -v, --version         Display version number               [デフォルト: false]
  -c, --compile         Compile the blueprint file           [デフォルト: false]
  -n, --include-path    Base directory for relative includes
  --verbose             Show verbose information and stack traces
                                                             [デフォルト: false]
  --theme-variables     Color scheme name or path to custom variables
                                                         [デフォルト: "default"]
  --theme-condense-nav  Condense navigation links      [真偽] [デフォルト: true]
  --theme-full-width    Use full window width         [真偽] [デフォルト: false]
  --theme-template      Template name or path to custom template
                                                         [デフォルト: "default"]
  --theme-style         Layout style name or path to custom stylesheet
  --theme-emoji         Enable support for emoticons   [真偽] [デフォルト: true]

例:
  node_modules/aglio/bin/aglio.js -i        Render to HTML
  example.apib -o output.html
  node_modules/aglio/bin/aglio.js -i        Start preview server
  example.apib -s
  node_modules/aglio/bin/aglio.js           Theme colors
  --theme-variables flatly -i example.apib
  -s
  node_modules/aglio/bin/aglio.js           Disable options
  --no-theme-condense-nav -i example.apib
  -s

See https://github.com/danielgtaylor/aglio#readme for more information

(コマンド例)

$ aglio -i src/index.md -p 8080 --server


複数ファイルの分割は <!-- include(xxx.md) --> で実現

参考:https://github.com/danielgtaylor/aglio#including-files

エントリーポイントとなるファイルで

FORMAT: 1A

<!-- include(xxx.md) -->

としておくと、別のファイルを読み込むことができる。
これで API 定義を複数のファイルに分割することができる。

これ自体は以前調べたときにも把握はしてたようだが、モックサーバー用ライブラリ api-mock の制限で断念したようだった。
が、次に示すようにモックサーバを api-mock から drakov というライブラリに変更したのでこの問題は解決した。


モックサーバには api-mock のかわりに drakov を使用

以前調べたときは api-mock を使おうとしていたらしいが、
今 Node のバージョン 7.1.0、npm のバージョン 3.10.9 でインストールしようとしたところエラーになってしまった。

$ npm i -g api-mock
/usr/local/bin/api-mock -> /usr/local/lib/node_modules/api-mock/bin/api-mock

> protagonist@1.2.6 install /usr/local/lib/node_modules/api-mock/node_modules/protagonist
> node-gyp rebuild

  CXX(target) Release/obj.target/libmarkdownparser/drafter/ext/snowcrash/ext/markdown-parser/src/ByteBuffer.o
clang: error: invalid deployment target for -stdlib=libc++ (requires OS X 10.7 or later)
make: *** [Release/obj.target/libmarkdownparser/drafter/ext/snowcrash/ext/markdown-parser/src/ByteBuffer.o] Error 1
gyp ERR! build error
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:276:23)
gyp ERR! stack     at emitTwo (events.js:106:13)
gyp ERR! stack     at ChildProcess.emit (events.js:191:7)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:215:12)
gyp ERR! System Darwin 14.5.0
gyp ERR! command "/usr/local/Cellar/node/7.1.0/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /usr/local/lib/node_modules/api-mock/node_modules/protagonist
gyp ERR! node -v v7.1.0
gyp ERR! node-gyp -v v3.4.0
gyp ERR! not ok

この Issue が関係している?Node のバージョン 6 系からうまくインストールできないらしい。
install failed at protagonist package · Issue #53 · localmed/api-mock

しかたないので
https://apiblueprint.org/tools.html#mock-servers
の一番上にあった drakov にした。

drakov は対象の markdown ファイルを指定する際

$ drakov -f "src/**/*.md"

というように正規表現で指定することができるので、ファイルが複数に分割されていても問題ない。
" でくくらないと期待通りに動かないので注意(シェル環境が zsh だったからかも)

また、--watch オプションをつけておくと markdown ファイルの変更を検知して自動的にリロードしてくれる。

(コマンド例)

$ drakov -f "src/**/*.md" -p 8081 --watch


TODO

結局いまだに dredd というツールを調べられていない。

React(v15)&ReduxでMaterial-UIを使ってみる

メモです。
基本的な使い方を試してみた。


リポジトリ

Redux の basic チュートリアル にそって作った Todo アプリをベースに、Material-UI を適用してみる。

https://github.com/zaki-yama/redux-todo-app/pull/2

(Material-UI 適用前のコードには base というタグをつけてます)

なお、公式 example リポジトリも存在するよう。

https://github.com/callemall/material-ui-webpack-example


インストール

$ npm install --save material-ui

でインストール。


導入手順

react-tap-event-plugin をインストールする

http://www.material-ui.com/#/get-started/installation によると、react-tap-event-plugin というパッケージが必要らしい。
Material-UI と同様に npm でインストールする。

$ npm install --save react-tap-event-plugin

このパッケージにより、Material-UI のすべてのコンポーネントは onTouchTap というイベントハンドラが使えるようになる。

マウスクリックやスマホタブレットのタッチ・タップイベントを共通のイベントハンドラで処理できるようになる、のかな?

また、上記ページには

This dependency is temporary and will go away once the official React version is released.

と書いており、試したときの React のバージョンは 15.3.2 だったので
メジャーバージョンが 1 以上だからこのプラグイン無しでも動くということかな?と思ったけど、結局インストールしないとうまく動かなかった。

react-tap-event-plugin をインストールした後は、以下のコードをアプリの先頭に記述する必要がある。

import injectTapEventPlugin from 'react-tap-event-plugin';

// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();

よって Redux の場合、アプリのエントリーポイント(src/index.js)に以下のように追記するはず。

 import { render } from 'react-dom';
 import { Provider } from 'react-redux';
 import { createStore } from 'redux';
+import injectTapEventPlugin from 'react-tap-event-plugin';
 import todoApp from './reducers';
 import App from './containers/App';
  
+// Needed for onTouchTap
+// http://stackoverflow.com/a/34015469/988941
+injectTapEventPlugin();
+
 const store = createStore(todoApp);
  
 render(
   <Provider store={store}>
     <App />
   </Provider>,
   document.getElementById('root'),
 );


Roboto フォントを適用する

またまた
http://www.material-ui.com/#/get-started/installation#roboto-font
を読むと、

Material-UI was designed with the Roboto font in mind

とあるので、Roboto フォント を読み込むようにする。

html ファイルに

<!-- src/index.html -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">

と書き、CSS でも

/* src/main.css */
html {
  font-family: 'Roboto', sans-serif;
}

とする。
確認したところ html 側の記述だけでも Material-UI コンポーネントに Roboto フォントが適用されるが、
アプリ全体のフォントを統一するため CSS でも指定することにした。

なお、話は逸れるが webpack で CSS を扱う方法はいくつか考えられる。

├── dist .................. ビルド後のファイルを置く場所
├── src
│   ├── actions
│   ├── components
│   ├── constants
│   ├── containers
│   ├── index.html
│   ├── index.js
│   ├── main.css
│   └── reducers
└── webpack.config.js

というディレクトリ構成の場合、

1) CSSCSS のままで html から読み込む場合

html 側は

<link rel="stylesheet" type="text/css" href="main.css">

とし、webpack.config.js では file-loader を使って dist ディレクトリに CSS ファイルもコピーされるようにする。

   context: __dirname + '/src',
   entry: {
     javascript: './index.js',
-    html: './index.html'
+    html: './index.html',
+    css: './main.css',
   },
   output: {
     path: __dirname + '/dist',
@@ -27,6 +28,10 @@ module.exports = {
       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
+      },
+      {
+        test: /\.css$/,
+        loader: 'file?name=[path][name].[ext]'
       }
     ]
   }

2) JavaScript にバンドルする場合

CSSJavaScript 内で import してしまうという場合、html の代わりに src/index.js で

import './main.css';

とし、webpack.config.js では css-loader と style-loader を使うようにする。

       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
-      }
+      },
+      {
+        test: /\.css$/,
+        loader: 'style!css',
+      },
     ]

どちらでもいいですが、2) の方が後々 SCSS とかにも応用できるので良さげ。


<MuiThemeProvider> でアプリケーション全体をラップする

コンポーネントを使う前の最後の準備として、アプリケーション全体を <MuiThemeProvider> というコンポーネントでラップする。
エントリーポイントになっている src/index.js でやってもいいと思うし、コンポーネントの root になっている containers/App.js でもいいはず。

containers/App.js

 import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+
 import AddTodo from './AddTodo';
 import VisibleTodoList from './VisibleTodoList';
 import FilterLinkList from '../components/FilterLinkList';

 export default class App extends React.Component {
   render() {
     return (
-      <div>
-        <AddTodo />
-        <VisibleTodoList />
-        <FilterLinkList />
-      </div>
+      <MuiThemeProvider>
+        <div>
+          <AddTodo />
+          <VisibleTodoList />
+          <FilterLinkList />
+        </div>
+      </MuiThemeProvider>
     );
   }
 }


Material-UI コンポーネントを使う

ここまでできるとようやく Material-UI のコンポーネントを使うことができる。

今回は簡単な例で、Todo の入力フォームとボタンをそれぞれ TextFieldRaisedButton に置き換えてみる。

元のコードは以下。

components/AddTodoForm.js

import React from 'react';

export default class AddTodoForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const node = this.refs.input;
    const text = node.value.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    node.value = '';
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <input ref="input" />
          <button type="submit">
            Add Todo
          </button>
        </form>
      </div>
    );
  }
}

これを以下のようにする。

import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';

export default class AddTodoForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: '',
    };
  }

  handleChange(e) {
    this.setState({
      input: e.target.value,
    });
  }

  handleSubmit(e) {
    e.preventDefault();
    const text = this.state.input.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    this.setState({
      input: '',
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <TextField
            id="todo-title"
            value={this.state.input}
            onChange={(e) => this.handleChange(e)}
          />
          <RaisedButton
            label="Add Todo"
            onTouchTap={(e) => this.handleSubmit(e)}
          />
        </form>
      </div>
    );
  }
}

フォームに入力されたテキストを管理するのに ref を使っていたのを local state で管理するようになったため全体のコード量が増えた。
ref のまま Material-UI のコンポーネントを管理することもできるみたいなんだけど、ドキュメントに載っていない方法なのでやめた。
あとで別の記事にでもメモする。

この変更によってアプリの UI は以下のように変更される。

before

f:id:dackdive:20161116014935p:plain

after

f:id:dackdive:20161116014944p:plain

ちょっとだけそれっぽい UI になった。


おわりに

ということで React/Redux なアプリに Material-UI を導入することができた。
意外と事前準備としてやることが多く、またそれがドキュメントからは微妙にわかりづらかったりして思った以上に手こずった。


トラブルシューティング的な

ボタンを使うと Unknown prop `onTouchTap` on <button> tag. が出る

→react-tap-event-plugin が入っていない。


リファレンス

eslint-config-airbnbのルール内容を確認する

ESLint はあまり深く考えず eslint-config-airbnb を導入していて、そんなに問題はないんだけども
たまにこちらが期待したルールが無効になってたり、逆にそんなに守らなくてもいいと思われるルールが有効になってたりして、ふと

「あれ、xxx っていうルールは今どういう設定になってるんだろう?」

と思うときがある。

で、eslint-config-airbnb の中身は実際どういうルールセットになっているのか確認してみた。


方法1:GitHub 上で直接確認する

この2つの rules を確認すれば良いと思われる。

が、複数のファイルに分かれていて、目的のルールにたどりつくのがめんどくさい。
git clone してローカルで grep するとか。


方法2:ESLint の --print-config オプションを使う

http://eslint.org/docs/user-guide/command-line-interface#print-config
によると ESLint には --print-config というオプションがあるらしく、

$ eslint --print-config file.js

というように通常の lint 実行時と同じく、対象ファイルを指定して実行する。

これを実行すると、対象ファイルに対して実際に lint チェックを行うかわりに
このファイルに対してどういった設定で lint チェックを行うか、設定(ルールセット)の方が出力される。

$ eslint --print-config index.js
{
  "globals": {
    "__dirname": false,
    "__filename": false,
    "arguments": false,
    "Buffer": false,
    "clearImmediate": false,
    ...
  "rules": {
    "accessor-pairs": 0,
    "array-callback-return": 2,
    ...
}

これを使えばいまどんなルールセットになっているのか知ることができる。

私は ESLint をグローバルインストールしていないので

$ ./node_modules/eslint/bin/eslint.js --print-config index.js | vim -R -

パスが面倒だけど↑のような感じで結果を Vim を開き、目的のルールを検索して設定内容を確認している。

--print-config は現在の設定内容を出力するので、当然のことながら .eslintrc に記述した内容は結果に反映される。
そういう意味ではオリジナルの eslint-config-airbnb のルールを確認する方法とはいえない。

[Vim]SyntasticによるESLintチェックが遅いのでNeomakeに乗り換えた

(2017/01/23追記)

この後 Flow を導入しようとしたら色々問題が発生したので、Neomake から ALE に乗り換えた。

(追記ここまで)


(2018/04/15追記)

現在、記事を書いた時と設定方法が変わっているようです。
こちらの方が最新の手順をまとめてくださっているので、ご参照ください。

(追記ここまで)


Vim の Syntax Checker として有名なのは Syntastic ですね。
最近は JavaScript を書くことが多いので、この Syntastic を使って ESLint のチェックをできるようにしていました。

上の記事に書いてある設定を行ったことで、Vim で常に lint チェックをかけられるようになったのは良かったんですが
1 個だけ不満があって、チェックのたびに操作がブロックされてしまうという問題がありました。

f:id:dackdive:20161023121542g:plain 正直こればっかりは我慢するしかないかなーと思ってたんですが、Neomake という Syntastic に替わるプラグインの存在を知ったので試してみたところ、かなり快適だったので紹介します。

なお、Vim でも NeoVim でもどっちでも動きました。
(動作確認した Vim のバージョンは 8.0.22)


インストール

プラグイン管理ツールを使っている場合、他のプラグインと同じようにインストールできます。
以下は、dein.vim を使っている場合の書き方です。

call dein#add('neomake/neomake')
autocmd! BufWritePost * Neomake " 保存時に実行する
let g:neomake_javascript_enabled_makers = ['eslint']

call dein#add('benjie/neomake-local-eslint.vim')

let g:neomake_error_sign = {'text': '>>', 'texthl': 'Error'}
let g:neomake_warning_sign = {'text': '>>',  'texthl': 'Todo'}

単純に動かすだけなら最初の3行だけでいいです。
残りの設定については以下の通り。


ESLint をグローバルインストールせずに使えるようにする

Syntastic でもやったアレ。プロジェクトごとにローカルインストールした ESLint だけで動くようにしたいものです。
同様のプラグインがありました。
https://github.com/benjie/neomake-local-eslint.vim

Neomake 本体と同様、インストールするだけで OK。

call dein#add('benjie/neomake-local-eslint.vim')


エラー行に表示されるアイコンをカスタマイズする

デフォルトで、エラーや警告のある行に表示されるアイコンはこんな感じ。

f:id:dackdive:20161023113225p:plain

カラースキームのせいもありますがあんまり目立ちませんね。
自分好みにカスタマイズしてみます。

:h naomake

でヘルプドキュメントを読むと、g:neomake_error_signg:neomake_warning_sign に設定するそうです。
また、デフォルトの設定は以下らしい。

let g:neomake_error_sign = {'text': '✖', 'texthl': 'NeomakeErrorSign'}
let g:neomake_warning_sign = {
     "\   'text': '⚠',
     "\   'texthl': 'NeomakeWarningSign',
     "\ }
let g:neomake_message_sign = {
     "\   'text': '➤',
     "\   'texthl': 'NeomakeMessageSign',
     "\ }

ESLint では error と warning を使ってるっぽいので、こうします。

let g:neomake_error_sign = {'text': '>>', 'texthl': 'Error'}
let g:neomake_warning_sign = {'text': '>>',  'texthl': 'Todo'}

text がカーソル行に表示される文字列で、どちらも Syntastic のときと同じ >> にしています。
texthlVim で定義済みのハイライトグループ(?) からグループ名を指定します。

このあたりはあんま詳しくないので、

:highlight

でグループ一覧を開き、Syntastic のときと同じ見た目になりそうな ErrorTodo を選びました。

f:id:dackdive:20161023114002p:plain

設定後の見た目。エラー行見やすくなった。

f:id:dackdive:20161023114101p:plain


使う

f:id:dackdive:20161023121728g:plain

伝わりづらいけど、保存時に lint チェックを実行しても編集処理がブロックされなくなりました。

また、Syntastic と同じく、エラー行にカーソルを持っていくと下部にエラー内容が表示されるだけでなく、

:lopen

でエラー一覧を location list で確認することもできます。(location list 内で Enter キーを押すと該当エラー行にジャンプできます)

f:id:dackdive:20161023122004p:plain

Reactでフォームの項目をどう扱うか問題

メモ。

なんの話か

  • React でフォーム項目を簡潔に書く方法がわからない
  • 管理されたコンポーネントで書こうとすると、項目の数だけ state と対応するイベントハンドラが必要になる
    • 親に渡す必要がある、とかだとさらにしんどい
// 基本形
class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };

    this.onChangeName = this.onChangeName.bind(this);
  }

  onChangeName(e) {
    console.log(e.target.value);
    this.setState({ name: e.target.value });
  }

  render() {
    return (
      <form>
        <input type="text" value={this.state.name} onChange={this.onChangeName} />
      </form>
    );
  }
}


どうすべきか

いくつか方法はある。
1 と 2 は微妙に違うが、「イベントハンドラを1つにまとめる」という戦略自体は同じ。

1. onChange イベントハンドラに引数で key を渡す
export default class MyFormCmp extends React.Component {

  constructor(props) {
    this.state = {
      name: '',
    };

    this.onChangeField = this.onChangeField.bind(this);
  }

  onChangeField(e, key) {
    console.log(key, e.target.value);
    this.setState({ [key]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          type="text" value={this.state.name} onChange={(e) => this.onChangeField(e, 'name')} />
      </form>
    );
  }
}


2. イベントハンドラ内で event.target.name を参照する

参考:ES2015 以降で React 書くなら form 部品での onChange で setState するのもう全部これでいいんじゃないかなあ - BattleProgrammerShibata

export default class MyFormCmp extends React.Component {
  // 略

  onChangeField(e) {
    console.log(e.target.name, e.target.value);
    this.setState({ [e.target.name]: e.target.value });
  }

  render() {
    return (
      <form>
        <input
          name="name"
          type="text"
          value={this.state.name}
          onChange={this.onChangeField}
        />
      </form>
    );
  }
}


3. (deprecated) LinkedStateMixin というアドオンを使う

参考:Two-Way Binding Helpers | React (日本語)

冒頭で

ReactLink is deprecated as of React v15. The recommendation is to explicitly set the value and change handler, instead of using ReactLink.

と書いているので使わないと思うけど。

加えて、ES2015 で書いた React コンポーネントはそのままでは Mixin を使うことができないので
react-mixin というライブラリを使う。

参考:React v0.13から使えるようになったES6のclass構文でmixinを使う - Qiita

import React from 'react';
import reactMixin from 'react-mixin';
import { render } from 'react-dom';
import LinkedStateMixin from 'react-addons-linked-state-mixin';

class MyFormCmp extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      name: '',
    };
  }

  render() {
    return (
      <form>
        <input
          type="text"
          valueLink={this.linkState('name')}
        />
      </form>
    );
  }
}
reactMixin(MyFormCmp.prototype, LinkedStateMixin);


4. (未検証) react-jsonschema-form を使う

元々は JSON Schema という、JSON オブジェクトの値の有無や型のチェックをするための仕様があって
react-jsonschema-form は JSON Schema からフォームを生成できるライブラリっぽい。

以下、JSON Schema の例。properties がフォームの項目に相当。

{
  "title": "A registration form",
  "description": "A simple form example.",
  "type": "object",
  "required": [
    "firstName",
    "lastName"
  ],
  "properties": {
    "firstName": {
      "type": "string",
      "title": "First name"
    },
    "lastName": {
      "type": "string",
      "title": "Last name"
    },
    "age": {
      "type": "integer",
      "title": "Age"
    },
    "bio": {
      "type": "string",
      "title": "Bio"
    },
    "password": {
      "type": "string",
      "title": "Password",
      "minLength": 3
    }
  }
}

実際にはこれとは別に UISchema なるものの定義が必要らしいが、ここから生成されるフォームはこんな感じ。
react-jsonschema-form playground で確認できる)

f:id:dackdive:20161019212542p:plain

いかんせんまだ触ったことがないので、スタイルやバリデーションルール含めどれぐらいきめ細かく設定ができるのか不明。


おわりに

3 は現在推奨されてないので除外。4 が一番気になってる。

1 と 2 については正直差はないと思う。
(JSX 側で state の key を知ってないといけない、という点で)

また、1 ~ 3 の方法は「入門 React」にも書いてた。(p.81 ~ p.84 あたり)