Реагировать.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/

6 ответов


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


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

редактировать 2015

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

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

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

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

резюме:

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

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

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

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

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

так вам нужна дополнительная строка, чтобы всякий раз, когда shouldComponentUpdate возвращает да, вы уверены, что содержимое 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 never rerenders (по крайней мере, при нажатии клавиш)

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

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

это происходит в основном, когда:

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

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

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

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

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


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

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

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