Electron+Reactで入力候補表示を実装した時のあれこれ

# やること

<img width="802" alt="スクリーンショット 2016-12-04 15.19.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/11673/b15cc43f-9690-a784-c8b2-8a848297933b.png">

こういうのをElectron+Reactでやりたかったので、実装してみました。

※ だいぶReact寄りの内容です。

実装内容としては

* 入力候補を表示する
* 候補となるのは以前に入力・登録した内容(= ユーザーの入力履歴によって動的に変わる)

といったところです。
formや使ったライブラリの仕様だったりで色々と苦戦したので、何かの参考になると嬉しいです。

# 実装

## 入力候補つきのフォームを作る

HTMLには ```<input autocomplete>``` という入力候補実装用の機能があります。
http://www.htmq.com/html5/input_autocomplete.shtml

これを使えば簡単に実現できるかなと思ってましたが、ReactのDOM更新との兼ね合いなのか思ったように動かず・・・。
Reactで同等の機能が実装されているライブラリを使います。

https://github.com/asbjornenge/react-datalist

```
$ npm install --save react-datalist
```

```js
import ReactDatalist from "react-datalist"
.
.
.
render() {
  return(
    <div>
      <form>
        <ReactDatalist
          forcePoly={true}
          list="listName"
          options={this.props.options} 
        />
      </form>
    </div>
  )
}
```

こんな感じで書いてあげると入力補完が効くフォームができます。
指定してるオプションは

* list: inputタグのlistのvalue, list用divのid名の指定
* options: 入力候補の配列
* forcePoly: Polyfillの有効化。これが入ってないと正しく表示が出ませんでした。

です。

## 入力候補の設定

次にoptionsに渡している値の設定の仕方です。
上記ソースにもあるとおり ```this.props``` を渡していて、親コンポーネントから更新されるようになっています。
また、今回はNeDBというモジュールをデータベースとして使っていて、ElectronのMainProcessでデータの読み書きをするようにしています。
なので、挙動としては
1. 子コンポーネントで入力が行われる
2. 親コンポーネントから渡された処理を実行
3. 2の処理内でipcRenderer.sendを実行し、MainProcessに入力データを送信
4. ipcMain.onで受け取り、データをNeDBに登録
5. 登録後のデータも含めて一覧取得して event.sender.sendでRendererProcessに渡す
6. ipcRenderer.onで受け取り、setStateして更新
7. 子コンポーネントのpropsが更新される
という流れになります。

この中で1,2あたりがクセがあるのでソースを含めて解説。

```js:
getController(controller) {
  this.setState({inputController: controller})
}

render() {
.
.
.
        <ReactDatalist
          forcePoly={true}
          list="listName"
          options={this.props.options}
          getController={this.getController.bind(this)}
        />
}
```

先ほどのreact-datalistコンポーネントにgetControllerという指定を加えます。
これでreact-datalist内の情報を取得したり、設定したりするControllerを取得できるようになります。
```this.getController()```の定義でもわかるように受け取ったControllerを```this.state.inputController```にセットして、利用できる状態にしています。
このControllerを経由することで入力内容を取得できます。
buttonを押したら入力結果を送信するようにします。

```js:render内button定義
<input
  type="button"
  onClick={this._onClick.bind(this)
/>
```

```js:buttonのonClick挙動の定義
_onClick() {
    if (this.state.inputController.getState().filter.length == 0) return ;
    this.props.onClickTrack(this.state.inputController.getState().filter)
    this.state.inputController.setState({filter:""})
}
```

先ほど取得したControllerの```getState()```でstateが取得できます。
inputタグへの入力内容はfilterという値に入っているので、その値を親コンポーネントから渡されたメソッドに渡し、filterの値は空にすることで登録と次の入力への準備ができます。

## formの振る舞いと戦う

HTMLのformは入力欄が１つのみの場合、Enterを押すとsubmit処理が走る仕様になっています。
便利な時もあるのですが、特にElectronだと更新が走ってしまうと値の受け渡し等がやりづらいです。
そこで、Enterを押した時のフォームのsubmitを止める必要があります。

```js
render() {
  return(
    <div>
      <form onKeyDown={this._onKeyDown.bind(this)}>
.
.
.
      </form>
    </div>
  )
}
```

formのonKeyDownイベントに対してリスナーを設定します。

```js:
_onKeyDown(e) {
  e.preventDefault();
}
```

preventDefault()はデフォルト仕様の挙動を回避する処理です。
これで、Enterでのフォームのsubmitを止めることができます。
ただしこれだと不便な部分もあります。それの解消が以下です。

## Enterで登録できるようにする

上記でフォームのsubmitは止められましたが、一方で入力が完了してEnterを押してもデータ登録はできず、ボタンを押さなくてはならない若干不便な状況になってしまいました。
なので、Enterでのフォームの送信は止めつつ、Enterでデータ登録はできるように修正します。

```js:
_onKeyDown(e) {
  if (e.keyCode != 13) return ;
  this._onClick()
  e.preventDefault()
}
```

2行追加しました。 ```this._onClick()```は 補完候補の設定の項のものと同じです。
preventDefault()の前でデータ登録処理を実行するようにしています。

それだけだとkeyDownのたびに送信が走ってしまうので、keyCodeでEnterを表す13以外の際にはそもそも処理が走らないようにしました。
ちなみにElectronはChromiumベースなので日本語変換の決定の際にはEnterキーを押しても229というkeyCodeが返ってくるので、日本語変換の決定時に送信処理が走ってしまうことはありません。

# まとめ

Electron + Reactで入力候補表示の実装およびハマったことのまとめでした。
クロスプラットフォームでのデスクトップアプリ開発ができるElectronは非常に面白い技術だと思っているので今後もいろんな開発をやってみようと思います。
また、最新のJavaScriptを試す環境としても単一のブラウザ環境しか考えなくていいElectronは良い選択肢だと思います。