web-dev-qa-db-ja.com

React.js:contentEditableのonChangeイベント

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/

90
NVI

Edit:Sebastien Lorber's answer を参照してください。これは私の実装のバグを修正します。


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;
    }
});
66
Brigand

2015年編集

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

Edit 06/2016:ブラウザーが、あなたが与えたばかりのhtmlをブラウザーが「再フォーマット」しようとしたときに発生する新しい問題をちょうど発見しました。コンポーネントは常に再レンダリングされます。 参照

Edit 07/2016:ここに私の本番のcontentEditable実装があります。 react-contenteditableには、次のような追加オプションがあります。

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

概要:

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

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

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

そのため、次のように思われます。

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

そのため、shouldComponentUpdateがyesを返すたびに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;
    }
});

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


詳細:

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

2)キーストロークでコンポーネントが再レンダリングされない場合、Reactはこのコンテンツの古い仮想domを編集可能にします。

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

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

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

これはおそらくあなたが探している答えではありませんが、自分でこれに苦労し、提案された答えに問題があるため、代わりに制御されないようにすることにしました。

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

/** @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;
14
Dan Abramov

これを行うには、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;
    }
}
4
klugjo

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;
}

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

2
Jeff

これは私にとって最も簡単な解決策です。

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

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

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