Reactを使って本気でアンケートシステムをつくった

この記事は エムスリー Advent Calendar 2017 の25日目の記事です。
普段はDB・サーバサイド・クライアントサイドまでの設計・実装・運用を扱っていますが、この記事ではReactを使って開発したシステムについてを紹介しようと思います。

# 作ったもの
アンケートシステム([survey-designer-js](https://github.com/jirokun/survey-designer-js))を作り、社内で使っていました。またOSSとしてレポジトリに公開もしています。

 * [GitHub](https://github.com/jirokun/survey-designer-js)
 * [DEMO](http://jirokun.github.io/survey-designer-js/)

なお公開しているのはクライアントサイドのみで、サーバサイドの実装は公開していません。なお、エムスリー社内で使用しているものはこのレポジトリからフォークしたものとなっています。

下記のような機能を備えています。

* ページの作成
    * ページ内への設問の作成
        * 複数選択肢
        * 単一選択肢(ラジオボタン)
        * 単一選択肢(プルダウン)
        * 数値記入
        * 1行テキスト
        * 複数行テキスト
        * 表形式
        * 都道府県
        * 説明文 / 表紙など
    * 設問共通機能
        * 任意入力指定
        * 項目のランダム
        * 項目のランダム時、固定指定
        * ユーザの入力値による選択肢の表示制御
        * ユーザの入力値による他の項目の入力値制限
        * ユーザの入力値の再掲処理
    * ロジック変数（計算処理）
    * JavaScriptの定義
    * フリーモードによるHTMLでのページデザイン(devモードのときのみに可能）
* ユーザの入力値による分岐条件の作成
    * デフォルトの遷移先の変更
    * 遷移図の描画（未マージ）
    * 遷移元の表示（未マージ）
* 終了ページの作成
    * 終了区分の設定(COMPLETE, SCREEN)
* プレビュー機能
    * 動作プレビュー
    * 詳細プレビュー

## 対応ブラウザ
エディタ: IE11以上, モダンなブラウザ
回答画面: IE9以上, モダンなブラウザ

# なんで作ったの？
他に使えるアンケートシステムがあればよかったのですが、下記のコンセプトを持ったシステムが他には見つからなかったので自分で作ることにしました。

 * エンジニアでなくても大部分のアンケートを作ることができる
    * アンケートを作成するためのGUIがそろっており、それなりのカスタマイズができることが条件
 * 複雑なものはエンジニアが手を加えることが可能で、基本作れないものは無いようにする
    * HTMLやjQueryなどを用いて案件ごとにカスタマイズできることが条件
    * アンケートの変更に強い実装にしたい

# 特徴・工夫など
## カスタマイズのためにHTMLやjQueryを利用できるようにする
要件として、HTMLやjQueryなどで案件ごとにカスタマイズできる仕組みが必要だったのですが、HTMLとjQueryを使うとReactをそのまま利用することができません。そこで、回答画面のページより先のコンポーネントはReactをuncontrolled componentとしてレンダリングして、一度レンダリングした後はplainなJavaScriptで制御を追加する形にしました。後からエンジニアがJavaScriptの処理を追加することもHTMLの書き換えも可能となっています。

## ページや選択肢を移動しても、アンケートが壊れないようにする
アンケートを作成していると、ページの順番を変えたり選択肢の順番を変えたりすることがあります。分岐条件などに設問の回答への参照を設定しますが、移動したときにも自動的に追従します。![move.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/69410478-483e-6fdd-1116-4bf08a256944.gif)

これは参照する値を設問番号などで持つのではなく、すべてIDで参照することで解決できます。また、同じ仕組みを利用することでHTML中に現れる文字列変換の仕組みも実現できます。![reprint.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/29ebff28-323e-4884-2e23-3aa79f192600.gif)

2番目の例ではユーザからは{{2-1-1.answer}}と入力させますが、内部的には項目2-1-1のIDに変換して持っています。
{{2-1-1.answer}} → {{cjbcdi4ct000i3h77sqlldv77.answer}}
このように持つことで、プレビューではrender時に再掲 2-1-1と変換して出力することが可能となり、回答時には実際のユーザの回答を表示することが可能です。

## 既存の設問のレイアウトを柔軟にカスタマイズしたい
これは開発者モードでのみ有効となるモードですが、HTML自体を編集することができます。この編集機能を利用すれば自由にレイアウトさせることが可能となります。
![page_customize.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/b593b1a4-c476-7891-5764-cf398bd79fdd.gif)

このエディタにはCodemirrorを採用しています。

## JavaScriptで処理を追加したい
ページ単位にJavaScriptを設定できるようにしています。通常用意していない機能を追加したり、複雑なvalidationや外部との通信などもここに記載できます。

## データのバリデーションや項目の表示制御が多くなりすぎて設定が辛いのをなんとかしたい
Excelでデータ入力する場合は同じようなデータをたくさん作ることは簡単だったけれども、Webシステムにすると入力が大変になってしまった、という話はよく聞きます。このアンケートシステムのエディタでも同様のことが起きるとわかっているので、データのバリデーションや項目の表示制御ではスプレッドシート([handsontable](https://handsontable.com/))を導入しました。

![spreadsheet.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/a0cbcd6a-1ce1-0389-c48e-885ef21c287e.gif)

ただし、下記の部分にはかなり苦労しています。
1点目はパフォーマンス問題です。これはReactはプロパティが更新されると再描画を行うように動作するため、handsontableのすべてのセルが更新されてしまうという問題があるためです。対策として下記の２つを取っています。

* 不要なときはそもそもhandsontableを表示しないようなUIにする。
* shouldComponentUdpateを適切に設定する

2点目はユーザが利用しやすいようするための苦労です。

* セルに入力できる値のバリデーション処理
* 選択した項目に対応するエディタの表示
* コピペができるようにするためのケア

まぁ単純に機能が多いということなんですが。。。Excelは偉大ですね。

## 表形式のテーブルをエンジニア無しにカスタマイズしたい
設問形式として表形式があるのですが、設問をUIから定義させるコンセプトでアンケートシステムを作成しているのでHTMLを書かせることはしません。WYSIWYGエディタの[TinyMCE](https://www.tinymce.com/)をテーブル部分のみに限定的に利用してカスタマイズできるようにしています。

![table_customize.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/eaa829bf-c251-9760-8411-93159aac6f3b.gif)

## エンジニアが使用するエディタでの開発効率を上げたい
Codemirrorを利用しているので、いわゆるショートカットキーやオートコンプリート機能を簡単に組み込むことができます。下記はオートコンプリート機能を利用してフォーム要素の自動補完を行っている例です。
![auto_complete.gif](https://qiita-image-store.s3.amazonaws.com/0/122922/7e47a111-a256-849e-950b-dd5cdcbf4e5f.gif)

# 最後に
Reactのチュートリアルなどでは簡易なアンケートシステムが取り上げられることが多いですが、本格的に作り込んだものは殆どないのではないかと思います。MITライセンスでGitHubに公開しているので、ぜひ使ってみてください。

誰かサーバサイドを作ってくれたりしないかな。