React.js: onChange событие для contentEditable

Как прослушать событие изменения для элемента управления 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/

Ответ 1

Изменить: Посмотрите ответ Себастьяна Лорбер, который исправляет ошибку в моей реализации.


Используйте событие 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;
    }
});

Ответ 2

Изменить 2015

Кто-то сделал проект NPM с моим решением: https://github.com/lovasoa/react-contenteditable

Редактировать 06/2016: Я только что начал новую проблему, возникающую, когда браузер пытается "переформатировать" html, который вы ему только что дали, что приводит к перепрограммированию компонента. Смотрите

Редактировать 07/2016: здесь мое производство contentEditable реализации. Он имеет некоторые дополнительные опции над react-contenteditable, которые могут вам понадобиться, в том числе:

  • замок
  • императивный API, позволяющий встраивать html-фрагменты
  • возможность переформатировать контент

Резюме:

Решение FakeRainBrigand для меня какое-то время отработало, пока у меня не возникло новых проблем. ContentEditables - это боль, и на самом деле нелегко справиться с React...

Этот JSFiddle демонстрирует проблему.

Как вы можете видеть, когда вы вводите некоторые символы и нажимаете Clear, содержимое не очищается. Это связано с тем, что мы пытаемся reset соответствовать контенту до последнего известного значения виртуального dom.

Так кажется, что:

  • Вам нужно shouldComponentUpdate, чтобы предотвратить скачки позиции каретки
  • Вы не можете полагаться на алгоритм React VDOM diff, если вы используете shouldComponentUpdate таким образом.

Итак, вам нужна дополнительная строка, чтобы всякий раз, когда 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;
    }
});

Виртуальный дом остается устаревшим и может быть не самым эффективным кодом, но по крайней мере он работает:) Моя ошибка решена


Детали:

1) Если вы помещаете shouldComponentUpdate во избежание переходов каретки, тогда контент-контент никогда не бывает rerenders (по крайней мере, нажатием клавиш)

2) Если компонент никогда не перезапускает ключевой штрих, тогда React сохраняет устаревший виртуальный dom для этого contenteditable.

3) Если React сохраняет устаревшую версию contenteditable в своем виртуальном дереве dom, тогда, если вы попытаетесь reset contenteditable к значению, устаревшему в виртуальном dom, тогда во время виртуального dom diff React вычислит, что нет никаких изменений в DOM!

Это происходит, главным образом, когда:

  • у вас есть пустой контент, доступный первоначально (shouldComponentUpdate = true, prop = "", предыдущий vdom = N/A),
  • пользователь вводит какой-либо текст и предотвращает визуализацию (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • После того, как пользователь нажмет кнопку проверки, вы хотите очистить это поле (shouldComponentUpdate = false, prop = ", предыдущий vdom =" ")
  • как и новый, так и старый vdom являются "", React не затрагивает dom.

Ответ 3

Это, вероятно, не совсем тот ответ, который вы ищете, но, борясь с этим сам и имея проблемы с предлагаемыми ответами, я решил сделать его неконтролируемым.

Когда editable prop является false, я использую text prop как есть, но когда он true, я переключаюсь в режим редактирования, в котором text не действует (но, по крайней мере, браузер не работает) t freak out). В течение этого времени элемент управления 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;

Ответ 4

Я предлагаю использовать 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;
    }
}

Ответ 5

Вот компонент, который включает в себя большую часть этого слова 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;
}

Я использую аналогичный подход успешно

Ответ 6

Вы можете сделать это для любого компонента, как это:

import SelectComponent from 'your folder'
    <SelectComponent 
       onChange={e => {
       console.log(e.value)
       }}
       >
       1
    <SelectComponent >

export default function SelectComponent({ onChange, ...other }) {
  const handleChange = value => {
    onChange({ value: value })
  }


  return (
    <div {...other}>
      <div onClick={handleChange}>123</div> // you can triger handleChange as you wish. Listen to data changes, mannualy or anything. 
    </div>
  )
}

Ответ 7

Это самое простое решение, которое сработало для меня.

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