React.js:contentEditableのonChangeイベント


120

contentEditableベースのコントロールの変更イベントをリッスンするにはどうすればよいですか?

var Number = React.createClass({
    render: function() {
        return <div>
            <span contentEditable={true} onChange={this.onChange}>
                {this.state.value}
            </span>
            =
            {this.state.value}
        </div>;
    },
    onChange: function(v) {
        // Doesn't fire :(
        console.log('changed', v);
    },
    getInitialState: function() {
        return {value: '123'}
    }    
});

React.renderComponent(<Number />, document.body);

http://jsfiddle.net/NV/kb3gN/1621/


11
私はこれに苦労し、提案された回答に問題があるため、代わりにそれを制御できないようにすることにしました。つまり、私はそれをに入れinitialValuestate使用しましたがrender、Reactにそれ以上更新させません。
Dan Abramov 2014年

JSFiddleが機能しません
Green

私はcontentEditable自分のアプローチを変更することで苦労を避けました- spanまたはの代わりに、属性とともにをparagraph使用inputしましたreadonly
Ovidiuさん-ミュウ

回答:


79

編集:私の実装のバグを修正するSebastien Lorberの回答を参照してください。


onInputイベントを使用し、オプションでonBlurをフォールバックとして使用します。余分なイベントの送信を防ぐために、以前の内容を保存したい場合があります。

個人的には、これをレンダー機能として使用します。

var handleChange = function(event){
    this.setState({html: event.target.value});
}.bind(this);

return (<ContentEditable html={this.state.html} onChange={handleChange} />);

jsbin

contentEditableの周りにこの単純なラッパーを使用しています。

var ContentEditable = React.createClass({
    render: function(){
        return <div 
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },
    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },
    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {

            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

1
@NVI、それはshouldComponentUpdateメソッドです。HTMLプロップが要素内の実際のHTMLと同期していない場合にのみジャンプします。例:あなたがした場合this.setState({html: "something not in the editable div"}})
ブリガンド

1
いいですが、this.getDOMNode().innerHTMLin への呼び出しshouldComponentUpdateはあまり最適化されていないと思います
Sebastien Lorber

@SebastienLorberはあまり最適化されていませんが、設定するよりもHTMLを読む方が良いと確信しています。私が考えることができる他の唯一のオプションは、htmlを変更する可能性のあるすべてのイベントをリッスンすることです。それらが発生すると、htmlをキャッシュします。ほとんどの場合、それはおそらく高速ですが、複雑さが増します。これは非常に確実でシンプルなソリューションです。
ブリガンド14年

3
state.html最後の「既知の」値に設定したい場合、これは実際にはわずかに欠陥があります。Reactに関する限り、新しいhtmlはまったく同じであるため、ReactはDOMを更新しません(実際のDOMは異なります)。jsfiddleを参照してください。私はこれに対する良い解決策を見つけていないので、どんなアイデアでも大歓迎です。
univerio 2014年

1
@dchest shouldComponentUpdateは純粋でなければなりません(副作用はありません)。
ブリガンド、2014年

66

2015年を編集

誰かが私のソリューションを使用してNPMでプロジェクトを作成しました:https : //github.com/lovasoa/react-contenteditable

2016年6月編集:ブラウザーが提供したhtmlをブラウザーが「再フォーマット」しようとすると、コンポーネントが常に再レンダリングされるという新しい問題が発生しました。見る

2016年7月を編集:これが私のcontentcontentEditable実装です。それはreact-contenteditableあなたが望むかもしれないことを含むいくつかの追加のオプションがあります:

  • ロック
  • HTMLフラグメントを埋め込むことができる命令型API
  • コンテンツを再フォーマットする機能

概要:

FakeRainBrigandのソリューションは、新しい問題が発生するまでしばらくの間、問題なく機能しました。ContentEditablesは面倒で、Reactを扱うのは本当に簡単ではありません...

このJSFiddleは問題を示しています。

ご覧のとおり、一部の文字を入力してをクリックしてClearも、コンテンツは消去されません。これは、contenteditableを最後の既知の仮想dom値にリセットしようとするためです。

だからそれはようです:

  • shouldComponentUpdateキャレット位置のジャンプを防ぐ必要があります
  • shouldComponentUpdateこの方法を使用する場合、ReactのVDOM差分アルゴリズムに依存することはできません。

したがって、追加の行が必要になるので、shouldComponentUpdateyesが返されるたびに、DOMコンテンツが実際に更新されます。

したがって、ここのバージョンはを追加して次のようcomponentDidUpdateになります。

var ContentEditable = React.createClass({
    render: function(){
        return <div id="contenteditable"
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },

    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },

    componentDidUpdate: function() {
        if ( this.props.html !== this.getDOMNode().innerHTML ) {
           this.getDOMNode().innerHTML = this.props.html;
        }
    },

    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {
            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

仮想domは古いままで、最も効率的なコードではないかもしれませんが、少なくとも機能します:) 私のバグは解決されました


詳細:

1)キャレットジャンプを回避するためにshouldComponentUpdateを配置した場合、contenteditableは再レンダリングされません(少なくともキーストロークでは)

2)コンポーネントがキーストロークで再レンダリングされない場合、Reactはこのcontenteditableの古い仮想domを保持します。

3)Reactが仮想domツリーでcontenteditableの古いバージョンを保持している場合、contenteditableを仮想domで古くなった値にリセットしようとすると、仮想dom diff中に、Reactは変更がないことを計算しますDOMに適用してください!

これは主に次の場合に発生します。

  • 最初に空のcontenteditableがある(shouldComponentUpdate = true、prop = ""、前のvdom = N / A)、
  • ユーザーがテキストを入力し、レンダリングを禁止します(shouldComponentUpdate = false、prop = text、previous vdom = "")
  • ユーザーが検証ボタンをクリックした後、そのフィールドを空にしたい(shouldComponentUpdate = false、prop = ""、previous vdom = "")
  • 新しく作成されたvdomと古いvdomはどちらも「」であるため、Reactはdomに触れません。

1
Enterキーが押されたときにテキストを警告するkeyPressバージョンを実装しました。jsfiddle.net/kb3gN/11378
Luca Colonnello、

@LucaColonnelloを使用{...this.props}すると、クライアントがこの動作を外部からカスタマイズできるようになります
Sebastien Lorber

ああ、これは良いです!正直なところ、私はこのソリューションを試し、divでkeyPressイベントが機能しているかどうかを確認しました!説明をありがとう
Luca Colonnello、2015年

1
shouldComponentUpdateコードがキャレットジャンプを防ぐ方法を説明できますか?
kmoe 2015

1
@kmoeは、contentEditableに適切なテキスト(キーストロークなど)がすでにある場合、コンポーネントが更新されないためです。ReactでcontentEditableを更新すると、キャレットがジャンプします。contentEditableなしで試してみてください;)
Sebastien Lorber

28

これは私のために働いた最も簡単な解決策です。

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>

3
これに反対投票する必要はありません、それは機能します!onInput例で述べたように使用することを忘れないでください。
セバスチャントーマス

素晴らしく、きれいです。多くのデバイスとブラウザで動作することを願っています!
JulienRioux

7
React状態でテキストを更新すると、キャレットが常にテキストの先頭に移動します。
Juntae

18

これはおそらくあなたが探している答えではないかもしれませんが、私自身これと格闘し、提案された答えに問題があるため、代わりにそれを制御できないようにすることにしました。

editablepropがの場合はfalse、prop をそのまま使用しますtextが、の場合はtruetext効果がない編集モードに切り替えます(ただし、少なくともブラウザーはフリークしません)。この間onChange、コントロールによって発砲されます。最後に、にeditable戻すとfalse、渡されたものでHTMLが埋められますtext

/** @jsx React.DOM */
'use strict';

var React = require('react'),
    escapeTextForBrowser = require('react/lib/escapeTextForBrowser'),
    { PropTypes } = React;

var UncontrolledContentEditable = React.createClass({
  propTypes: {
    component: PropTypes.func,
    onChange: PropTypes.func.isRequired,
    text: PropTypes.string,
    placeholder: PropTypes.string,
    editable: PropTypes.bool
  },

  getDefaultProps() {
    return {
      component: React.DOM.div,
      editable: false
    };
  },

  getInitialState() {
    return {
      initialText: this.props.text
    };
  },

  componentWillReceiveProps(nextProps) {
    if (nextProps.editable && !this.props.editable) {
      this.setState({
        initialText: nextProps.text
      });
    }
  },

  componentWillUpdate(nextProps) {
    if (!nextProps.editable && this.props.editable) {
      this.getDOMNode().innerHTML = escapeTextForBrowser(this.state.initialText);
    }
  },

  render() {
    var html = escapeTextForBrowser(this.props.editable ?
      this.state.initialText :
      this.props.text
    );

    return (
      <this.props.component onInput={this.handleChange}
                            onBlur={this.handleChange}
                            contentEditable={this.props.editable}
                            dangerouslySetInnerHTML={{__html: html}} />
    );
  },

  handleChange(e) {
    if (!e.target.textContent.trim().length) {
      e.target.innerHTML = '';
    }

    this.props.onChange(e);
  }
});

module.exports = UncontrolledContentEditable;

他の回答で抱えていた問題を詳しく説明していただけますか?
NVI 2014年

1
@NVI:注入に対する安全性が必要なので、HTMLをそのまま配置することはできません。HTMLを配置せずにtextContentを使用すると、ブラウザーのあらゆる種類の不整合が発生し、shouldComponentUpdate簡単に実装できなくなるため、キャレットジャンプから救うことさえできません。最後に、CSS疑似要素の:empty:beforeプレースホルダーがありますが、このshouldComponentUpdate実装により、ユーザーがフィールドをクリアしたときにFFおよびSafariがフィールドをクリーンアップできなくなりました。制御されていないCEでこれらすべての問題を回避できることを理解するのに5時間かかった。
Dan Abramov 2014年

どのように機能するのか、私にはよくわかりません。あなたは決して変更editableの中でUncontrolledContentEditable。実行可能な例を提供できますか?
NVI 2014年

@NVI:ここではReact内部モジュールを使用しているので少し難しいです。基本的にeditableは外部から設定します。ユーザーが「編集」を押すとインラインで編集でき、ユーザーが「保存」または「キャンセル」を押すと再び読み取り専用になるフィールドを考えてください。したがって、読み取り専用の場合は小道具を使用しますが、「編集モード」に入るたびに小道具を見ることをやめ、終了するときにのみ小道具を見ます。
Dan Abramov 2014年

3
このコードを使用するユーザーのために、Reactの名前escapeTextForBrowserをに変更しましたescapeTextContentForBrowser
2015年

8

編集が完了すると、要素からのフォーカスは常に失われるため、単にonBlurフックを使用できます。

<div onBlur={(e)=>{console.log(e.currentTarget.textContent)}} contentEditable suppressContentEditableWarning={true}>
     <p>Lorem ipsum dolor.</p>
</div>

5

これを行うには、mutationObserverを使用することをお勧めします。それはあなたに何が起こっているかについてより多くの制御を与えます。また、ブラウズがすべてのキーストロークをどのように解釈するかについての詳細も提供します。

ここTypeScriptで

import * as React from 'react';

export default class Editor extends React.Component {
    private _root: HTMLDivElement; // Ref to the editable div
    private _mutationObserver: MutationObserver; // Modifications observer
    private _innerTextBuffer: string; // Stores the last printed value

    public componentDidMount() {
        this._root.contentEditable = "true";
        this._mutationObserver = new MutationObserver(this.onContentChange);
        this._mutationObserver.observe(this._root, {
            childList: true, // To check for new lines
            subtree: true, // To check for nested elements
            characterData: true // To check for text modifications
        });
    }

    public render() {
        return (
            <div ref={this.onRootRef}>
                Modify the text here ...
            </div>
        );
    }

    private onContentChange: MutationCallback = (mutations: MutationRecord[]) => {
        mutations.forEach(() => {
            // Get the text from the editable div
            // (Use innerHTML to get the HTML)
            const {innerText} = this._root; 

            // Content changed will be triggered several times for one key stroke
            if (!this._innerTextBuffer || this._innerTextBuffer !== innerText) {
                console.log(innerText); // Call this.setState or this.props.onChange here
                this._innerTextBuffer = innerText;
            }
        });
    }

    private onRootRef = (elt: HTMLDivElement) => {
        this._root = elt;
    }
}

2

これは、lovasoaによってこれの多くを組み込んだコンポーネントです。https://github.com/lovasoa/react-contenteditable/blob/master/index.js

彼は、emitChangeでイベントをシムします

emitChange: function(evt){
    var html = this.getDOMNode().innerHTML;
    if (this.props.onChange && html !== this.lastHtml) {
        evt.target = { value: html };
        this.props.onChange(evt);
    }
    this.lastHtml = html;
}

私は同様のアプローチをうまく使用しています


1
著者は、package.jsonで私のSOの回答を信用しています。これは私が投稿したコードとほぼ同じであり、このコードが私のために機能することを確認します。github.com/lovasoa/react-contenteditable/blob/master/...
セバスチャン・ローバー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.