概要
今までは純粋なStringで管理していたBlogの記事内容を、Wysiwyg編集できるようにするときにした作業の中身についてまとめました!
↓
技術背景
- 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も足しておいたので、日本語にも対応してます!
実装
Draft.jsを入れる
npm install --save draft-js
npm install --save react-draft-wysiwyg
Wysiwyg化済みかどうか識別するFlagをテーブルに追加
既存の動作しているテーブルに対しての機能拡張なため、Flagを用意します。
add_column :blogs, :is_wysiwyg, :boolean, default: false
編集Componentの作成
form_forをそのまま使えるようにtextareaでid、nameを入れながら出力するようにしました。
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の修正
= form_for @blog, url: blog_path(@blog.public_id) do |f|
= f.text_area :content
↓
= 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で調整しました。
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
- Draft.js and stateToHTML, how to render output html in react component?
その他
Stringで表示したい対応
もともとStringで動いていたシステムなのでStringデータが欲しい箇所が出てきます。
(truncateしながら概要を一部出している箇所など)
そのままだとjson構文のまま生で出てしまうので、Stringで出力する関数も作成しました。
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
完成!
※ツールバーやcssはよしなに調整しています。form_forで使えるので、他のModelでも気軽に使い回せるEditor Componentができました!
参考
- Draft.js と Slate.js と CodeMirror の感想