[React] Draft.jsでRailsサービスにWysiwygエディタをつけてみた

# 概要
今までは純粋なStringで管理していたBlogの記事内容を、Wysiwyg編集できるようにするときにした作業の中身についてまとめました！
<img width="593" alt="Screen Shot 2018-01-18 at 19.10.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/74964/80a17857-46eb-b856-4e93-d570830f7436.png">
↓
<img width="738" alt="Screen Shot 2018-01-18 at 19.03.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/74964/9250f900-1ea6-1c34-162c-abc4e6aa90ff.png">

## 技術背景
- rails
- react-rails

react-railsの導入方法については本題から逸れるため、この記事からは省きます。
上記技術背景のプロジェクトに置いて、下記技術を導入しました。

### Draft.js
ReactでWysiwygエディタを作るとしたら「これ」というもの。
Facebook製なため人気も高く、Ecosystemが整いつつあり、簡単に導入できかつ要件に合わせて機能追加が容易です。

### react-draft-wysiwyg
Draft.js製のStand Alone Editorは様々あり、Mediumのような形式からツールバーが上に出てる旧来の形式まで様々です。
https://github.com/nikgraf/awesome-draft-js#standalone-editors-built-on-draftjs

今回はその中でも、react-draft-wysiwygを選択しました。
理由はツールバーが文字上に出るMediumのような形式だとSPで使いづらかったこと、そしてツールバーが上部固定のものの中で好みで選択しました。

[日本語のTranslationFileも足しておいた](https://github.com/jpuri/react-draft-wysiwyg/pull/586)ので、日本語にも対応してます！

# 実装
## Draft.jsを入れる
```
npm install --save draft-js
npm install --save react-draft-wysiwyg
```

## Wysiwyg化済みかどうか識別するFlagをテーブルに追加
既存の動作しているテーブルに対しての機能拡張なため、Flagを用意します。

```XXXX_add_is_wysiwyg_to_blogs.rb
add_column :blogs, :is_wysiwyg, :boolean, default: false
```

## 編集Componentの作成
form_forをそのまま使えるようにtextareaでid、nameを入れながら出力するようにしました。

```RichEditor.js
import { Editor } from 'react-draft-wysiwyg'
import { EditorState, ContentState, convertToRaw, convertFromRaw, convertFromHTML } from 'draft-js'

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

    let editorState

    try {
      if (this.props.isJsonObject) {
        editorState = EditorState.createWithContent(convertFromRaw(JSON.parse(this.props.defaultValue)))
      } else {
        editorState = EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue))
      }
    } catch (e) {
      editorState = EditorState.createEmpty()
    }

    this.state = {
      editorState: editorState
    }
  }

  render() {
    const { editorState } = this.state;
    return (
      <div>
        <Editor
          editorState={ editorState }
          onEditorStateChange={(editorState) => { this.setState({ editorState }) }}
        <textarea
          hidden
          name={ `${this.props.model}[${this.props.column}]` }
          id={ `${this.props.model}_${this.props.column}` }
          value={ JSON.stringify(convertToRaw(editorState.getCurrentContent())) }
        />
      </div>
    )
  }
}

RichEditor.propTypes = {
  model:        PropTypes.string,
  column:       PropTypes.string,
  defaultValue: PropTypes.string,
  isJsonObject: PropTypes.bool
}

RichEditor.defaultProps = {
  model:        '',
  column:       '',
  defaultValue: '',
  isJsonObject: true
}
```

## Viewの修正
```edit.slim
= form_for @blog, url: blog_path(@blog.public_id) do |f|
  = f.text_area :content
```
↓

```edit.slim
= form_for @blog, url: blog_path(@blog.public_id) do |f|
  = react_component 'RichEditor', {defaultValue: @blog.content, isJsonObject: @blog.is_wysiwyg, model: 'blog', column: 'content'}
  = f.hidden_field :is_wysiwyg, value: 1
```

## 表示Componentの作成
そのままだと見え方がEditorなのでclass指定してcssで調整しました。

```RichViewer.js
import { EditorState, convertFromRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'

export default class RichViewer extends React.Component {
  constructor(props) {
    super(props)
    let editorState

    try {
      editorState = EditorState.createWithContent(convertFromRaw(JSON.parse(this.props.value)))
    } catch (e) {
      editorState = EditorState.createEmpty()
    }

    this.state = {
      editorState: editorState
    }
  }

  render() {
    const { editorState } = this.state;
    return (
      <Editor
        toolbarHidden
        readOnly={ true }
        editorClassName="rdw-viewer-editor"
        toolbarClassName="rdw-viewer-toolbar"
        editorState={ editorState }
      />
    )
  }
}

RichViewer.propTypes = {
  value: PropTypes.string
}
RichViewer.defaultProps = {
  value: ''
}
```

※Editorで表示するのには違和感があるけど、方法論として間違ってはいなさそうです。

- How to create a draft.js "viewer" · GitHub
  - https://gist.github.com/ditlevtojner/17d65e9884d688eb383d
- Draft.js and stateToHTML, how to render output html in react component?
  - https://stackoverflow.com/questions/43914432/draft-js-and-statetohtml-how-to-render-output-html-in-react-component

# その他
## Stringで表示したい対応
もともとStringで動いていたシステムなのでStringデータが欲しい箇所が出てきます。
（truncateしながら概要を一部出している箇所など）
そのままだとjson構文のまま生で出てしまうので、Stringで出力する関数も作成しました。

```blog.rb
  def string_content
    if self.is_wysiwyg
      parsed_content = JSON.parse(self.content)
      if parsed_content['blocks'].present?
        parsed_content['blocks'].map{|block| block['text'].present? ? block['text'] : ''}.join("\n\n")
      else
        ''
      end
    else
      self.content
    end
  end
```

js側ならContentStateのgetPlainTextから取得可能です。
https://draftjs.org/docs/api-reference-content-state.html#getplaintext

# 完成！
<img width="738" alt="Screen Shot 2018-01-18 at 19.03.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/74964/9250f900-1ea6-1c34-162c-abc4e6aa90ff.png">
※ツールバーやcssはよしなに調整しています。

form_forで使えるので、他のModelでも気軽に使い回せるEditor Componentができました！

# 参考
- Draft.js と Slate.js と CodeMirror の感想
  - https://medium.com/@r7kamura/draft-js-%E3%81%A8-slate-js-%E3%81%A8-codemirror-a7ef80d69998