React.js, jQuery-ui, Autocompleteで語句候補ドロップダウン

![Screenshot 2015-07-10 22.15.40.png](https://qiita-image-store.s3.amazonaws.com/0/82804/f0b65c10-138e-f56b-cd46-ba80fd988fca.png)

#概要

- [React.js](https://facebook.github.io/react/)の入力フォームで[jQuery-ui/Autocomplete](https://jqueryui.com/autocomplete/)を利用し、利用者の文字列入力に応じて語句の候補をドロップダウンで表示する機能を追加する。
- Rails, HAML, CoffeeScriptで書いた。

#環境

```
ruby 2.2.1
Rails 4.2.1
```

#準備（Rails場合のー例）

##'jquery-ui-rails'をGemfileに追加

```rb:主なGem
#jQuery-ui/Autocompleteを使うため。
gem 'jquery-ui-rails', '~> 5.0.5'

#React.jsを使うため。
gem 'react-rails',     '~> 1.0.0'

# ...他
```

##application.jsにjquery-ui/autocompleteを追加

```js:application.js
// 例
//= require jquery
//= require jquery_ujs
//= require jquery.turbolinks
//= require bootstrap
//= require turbolinks
//= require jquery-ui/autocomplete
//= require react
//= require react_ujs
//= require growl
//= require components
//= requre_tree .
```

#新規データ入力フォーム　処理の流れ

- 表作成用のデータとともに、カテゴリー名称と部屋名称のデータをデータベースから取り出し、配列として部品レンダリング時に渡しておく。
- ユーザーが入力した内容に反応してその文字を含む語句を候補として表示する。
- 語句をクリックすると、それが入力データとして取り扱われる。
- データを検証し、OKであれば、提出ボタンが有効になり、提出可能になる。
- データはXHRでサーバーに送信され、データベースに保存される。
- 部品のUIも状態データを元に更新される。（新規データ）が表に追加される。

##1. メインの部品

```coffeescript:RecordsApp.coffee

@RecordsApp = React.createClass
  # レンダリング時に受け取ったデータ（表を作成するデータ）を覚えておく。
  getInitialState: ->
    records: @props.data

  getDefaultProps: ->
    records: []

  # 新規作成用フォームに入力された内容でデータを追加する。そしてUIを更新。
  addRecord: (record) ->
    records = React.addons.update(@state.records, { $unshift: [record] })
    @setState records: records

  #（中略）

  render: ->
    # React.DOM記述を省略する目的の一時変数。
    R = React.DOM

    R.div
      R.h2 null, "Add a new item"

      # 新規作成フォーム
      React.createElement NewMovingRecordForm,
        # 新規データ処理用のメソッドを渡す。
        handleNewRecord: @addRecord
        # 語句候補のデータを渡す。
        roomSuggestions: @props.roomSuggestions
        categorySuggestions: @props.categorySuggestions
      R.hr null

      # 表
      React.createElement Records,
        records: @state.records,
```

##2. 新規作成フォーム部品

```coffeescript:NewRecordForm.js.coffee
@NewMovingRecordForm = React.createClass
  # 元の状態
  getInitialState: ->
    name:        ""
    volume:      ""
    quantity:    ""
    room:        ""
    category:    ""
    description: ""

  # 一字一句入力時に状態データを更新する。
  handleChange: (e) ->
    name = e.target.name
    @setState "#{ name }": e.target.value

  # 提出ボタンが押された時の処理をここに記述する。
  handleSubmit: (e) ->

  # UIを元の状態（空フォーム）に戻す。
  handleClear: (e) ->
    @setState @getInitialState()

  # 入力内容の検証
  valid: ->
    @validName() && @validVolume() && @validQuantity() &&
    @validRoom() && @validCategory() && @validDescription()
  validName: ->
    @state.name && @state.name.length <= 50
  validVolume: ->
    @state.volume && @state.volume.length <= 10
  validQuantity: ->
    @state.quantity && @state.quantity.length <= 10
  validRoom: ->
    @state.room && @state.room.length <= 50
  validCategory: ->
    @state.category && @state.category.length <= 50
  validDescription: ->
    @state.description.length <= 200

  # Reactによりマークアップが生成された直後に呼ばれる。ここでjQueryが実際のDOMに対してAutocompleteを初期化する。(Reactは知らない）
  componentDidMount: ->
    @updateAutocomplete()

  # Reactによりマークアップが更新された直後に呼ばれる。ここでAutocompleteを更新する。
  componentDidUpdate: ->
    @updateAutocomplete()

  # jQueryが作ったAutocompleteを消去する。
  componentWillUnmount: ->
    $(React.findDOMNode(@refs.room)).autocomplete('destroy')
    $(React.findDOMNode(@refs.category)).autocomplete('destroy')

  # Autocomplete初期化・更新の処理。
  updateAutocomplete: ->
    $(React.findDOMNode(@refs.room)).autocomplete
      source: @props.roomSuggestions
      select: (e, ui) =>
        @setState room: ui.item.value

    $(React.findDOMNode(@refs.category)).autocomplete
      source: @props.categorySuggestions
      select: (e, ui) =>
        @setState category: ui.item.value

  render: ->
    R = React.DOM

    R.form
      onSubmit:  @handleSubmit
      R.div
        className: 'form-group'
        R.div
          className: "form-group col-sm-12"
          R.input
            type:        'text'
            className:   'form-control'
            placeholder: 'Item name'
            name:        'name'
            value:       @state.name
            onChange:    @handleChange
        R.div
          className: "form-group col-sm-6"
          R.input
            type:        'number'
            min:         "0"
            className:   'form-control'
            placeholder: 'Volume'
            name:        'volume'
            value:       @state.volume
            onChange:    @handleChange
        R.div
          className: "form-group col-sm-6"
          R.input
            type:        'number'
            min:         "0"
            className:   'form-control'
            placeholder: 'Quantity'
            name:        'quantity'
            value:       @state.quantity
            onChange:    @handleChange
        R.div
          className: "form-group col-sm-6"
          R.input
            type:        'text'
            className:   'form-control'
            ref:         'category'  # AutocompleteがDOMアクセスに使用
            name:        'category'  # @handleChange処理時に使用
            placeholder: 'Category'
            value:       @state.category
            onChange:    @handleChange
        R.div
          className: "form-group col-sm-6"
          R.input
            type:        'text'
            className:   'form-control'
            ref:         'room'  # AutocompleteがDOMアクセスに使用
            name:        'room'  # @handleChange処理時に使用
            placeholder: 'Room'
            value:       @state.room
            onChange:    @handleChange
        R.div
          className: "form-group col-sm-6"
          R.textarea
            rows:        '3'
            className:   'form-control'
            placeholder: 'Description'
            name:        'description(optional)'
            value:       @state.description
            onChange:    @handleChange

        R.div
          className: 'col-sm-6'
          R.div
            className: 'form-group col-sm-8'
            R.button
              type:      'submit'
              className: if @valid() then 'btn btn-success btn-block' else 'btn btn-default btn-block'
              disabled:  not @valid()
              'Add item'
          R.div
            className: 'form-group col-sm-4'
            R.button
              type:      'submit'
              className: "btn btn-default btn-block"
              onClick: @handleClear
              'Clear'
        R.div
          className: 'form-group col-sm-6'
          R.span
            id: "helpBlock"
            className: "help-block text-center"
            "Please fill in all the required fields"
      R.div className: "clearfix"
```

##3. ドロップダウンメニューのスタイリング

自分の好みでスタイリングする。

```scss:autocomplete.scss
ul.ui-autocomplete {
  position: absolute;
  list-style: none;
  margin: 0;
  padding: 0;
  border: solid 1px #999;
  cursor: default;
  li {
    background-color: #FFF;
    border-top: solid 1px #DDD;
    margin: 0;
    padding: 2px 15px;
    a {
      color: #000;
      display: block;
      padding: 3px;
    }
    a.ui-state-hover, a.ui-state-active {
      background-color: #FFFCB2;
    }
  }
}
```

##4. 語句候補のデータを渡しレンダリング

以下の例はreact_railsのreact_componentメソッドを使用。
語句候補のデータを予め渡してレンダリングする。部品内部では@propsを介してアクセスする。

```haml:show.html.haml
%h1.page-header Sample template

= react_component 'RecordsApp', { data: @items,
                   roomSuggestions: Room.all.pluck("name"),
                   categorySuggestions: @moving.moving_items.pluck("category").uniq! }
```

#参考資料

- https://jqueryui.com/autocomplete/
- https://github.com/railscasts/102-auto-complete-association-revised
- http://ludovf.net/reactbook/blog/reactjs-jquery-ui-autocomplete.html
- https://facebook.github.io/react/docs/more-about-refs.html