it-swarm.com.ru

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

Как прослушать событие change для contentEditable-based элемента управления?

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/

88
NVI

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


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

Изменить 2015

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

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

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

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

Резюме:

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

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

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

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

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

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


Подробности:

1) Если вы установите флажок shouldComponentUpdate, чтобы избежать скачков каретки, то contenteditable никогда не будет повторно отображаться (по крайней мере, при нажатии клавиш)

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

3) Если React сохраняет устаревшую версию contenteditable в своем виртуальном dom-дереве, то если вы попытаетесь сбросить 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 являются "", React не касается dom.
60
Sebastien Lorber

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

Когда editable prop равен 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;
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
Abhishek Kanthed