WYSIWYGエディタ を React コンポーネントにしてみました

以前、Ruby勉強会で開発中のアプリケーションに利用するため、MarkdownエディタをReactコンポーネント化したことを、紹介させていただいたことがありました。(前の記事はこちら)

「社員からMarkdownじゃなくて、そのままHTMLを編集できるようにしてほしい。将来製品化するなら使いやすくないとね。」と言われてしまいました。
技術者ならMarkdownくらい・・・と思いますが、確かに今後技術者じゃない方も利用することも考えると納得できる部分もあり、早速WYSIWYGエディタを物色してみました。が、フリー且つ使いやすそうなものがなかなか見つからず、機能が豊富なCKEditorなるものにトライしました。しかし、jsファイルが多くどうやらJavaScriptファイルから別のJavaScriptファイルを呼び出しているなどで、RailsやReactで利用するには難しいと判断し、こちらは早々にあきらめました。

初めは欲張らずにシンプルな機能のもので試してみようということで、Trumbowygを採用してみました。
特にこだわった理由はありませんが、軽量(16kb以下)であることと、ドキュメントが少ないながら必要な内容はしっかり書かれていた点でしょうか。

bowerからインストールしてみましたが、インストールされたバージョンは1.1.6(安定版?)。編集時のchangeイベントが取得できなかったため、まだBeta版となっていましたがchangeイベントが利用できる2.0.0を利用してみることにしました。
こちらはアーカイブをダウンロードして必要なディレクトに展開します。

以前、MarkdownエディタをReactコンポーネントにしたこともあり、すぐに利用できるようになるかと思っていましたが、ソースコードを眺めながら思い出すのに時間がかかりましたが、なんとか半日程度で利用できる目途は立ちました。

ソースを載せておきます。実際に利用する際は、このコンポーネントを更にReactから呼び出して利用するような感じですね。

補足を付け加えておくと、

  • componentWillMount
    ここで、textareaの要素を生成していますが、Webページから通常のフォームのようにコンテンツを送信できるようにするため、 入力されたRAWデータをこのtextareaに保持させています。

  • componentDidMount
    ここではエディタが表示された後に、変更イベントを受け取る関数の設定を行っています。
    念のため、フォーカスが外れた時も設定しておきました。

  • componentDidUpdate
    この関数はコンポーネントの状態(State)に変化があった時に呼び出されますが、ここがReactでいう「行儀の悪い」に対応するための処理です。もしかしたらTrumbowygは問題ないのかもしれませんが、以前のMarkdownエディタと同じような処理にしておきました。将来別のエディタに変更するかもしれませんので。

(function() {
  $(function() {
    "use strict";

    var WysiwygEditor = React.createClass({
      propTypes: {
        onChange: React.PropTypes.func,
        name: React.PropTypes.string,
        textValue: React.PropTypes.string
      },
      getDefaultProps: function() {
        return {
          textValue: ''
        };
      },
      getInitialState: function() {
        return {
          textValue: this.props.textValue
        };
      },
      componentWillMount: function() {
        if (this.ta) {
          return;
        }
        this.ta = document.createElement('textarea');
        this.ta.setAttribute("name", this.props.name);
        this.ta.value = this.props.textValue;
        $(this.ta).hide();
      },
      componentDidMount: function() {
        React.findDOMNode(this).appendChild(this.ta);

        var child = React.findDOMNode(this.refs.editor);
        this.editor = $(child).trumbowyg();
        this.editor
          .on('tbwchange', this.handleChange)
          .on('tbwblur', this.handleChange);

        this.editor.trumbowyg('html', this.props.textValue);
      },
      handleChange: function(event) {
        if (this.props.onChange) {
          this.props.onChange(event);
        }
        var html = $(React.findDOMNode(this.refs.editor)).trumbowyg('html');
        this.setState({textValue: html});
        this.ta.value = html;
      },
      componentWillReceiveProps: function() {
        this.setState({textValue: this.props.textValue});
      },
      componentDidUpdate: function() {
        if ($(React.findDOMNode(this.refs.editor)).trumbowyg('html') === this.props.textValue) return;
        this.componentWillUnMount();
        this.componentWillMount();
        this.componentDidMount();
      },
      componentWillUnMount: function () {
        $(this.editor).off();
        React.findDOMNode(this).removeChild(this.ta);
        this.editor = null;
        this.ta = null;
      },
      render: function() {
        return (
            <div><div ref="editor" /></div>
          );
      }
    });
    window.RCWysiwygEditor = WysiwygEditor;
  });
}).call(this);

バリデーションなど、まだ不十分な部分もあるかと思いますが動作検証はできました。
Trumbowygにもう少し機能が増えてくればこのまま利用できそうな気もします。