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

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/

NVI
источник
11
Сам борясь с этим и имея проблемы с предлагаемыми ответами, я решил вместо этого сделать это неконтролируемым. То есть, я положил initialValueв stateи использовать его в render, но я не позволяю Реагировать обновления его дальше.
Дэн Абрамов
Ваш JSFiddle не работает
Green
Я избежал contentEditableпроблем, изменив свой подход - вместо spanили paragraphя использовал inputвместе с его readonlyатрибутом.
ovidiu-miu 08

Ответы:

79

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


Используйте событие 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"}})
Brigand
1
хорошо , но я предполагаю , что вызов this.getDOMNode().innerHTMLв shouldComponentUpdateне очень оптимизирован право
Себастьен Лорбер
@SebastienLorber не очень оптимизирован, но я уверен, что лучше прочитать html, чем устанавливать его. Единственный другой вариант, который я могу придумать, - это прослушать все события, которые могут изменить html, и когда это произойдет, вы кешируете html. Это, вероятно, будет быстрее в большинстве случаев, но добавит много сложности. Это очень верное и простое решение.
Brigand
3
На самом деле это немного ошибочно, когда вы хотите установить state.htmlпоследнее «известное» значение, React не будет обновлять DOM, потому что новый html точно такой же, как и React (даже если фактическая DOM отличается). См. Jsfiddle . Я не нашел для этого хорошего решения, поэтому любые идеи приветствуются.
univerio
1
@dchest shouldComponentUpdateдолжен быть чистым (не иметь побочных эффектов).
Brigand
66

Изменить 2015

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

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

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

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

Резюме:

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

Этот 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;
    }
});

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


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

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

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

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

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

  • изначально у вас есть пустой контент (shouldComponentUpdate = true, prop = "", предыдущий vdom = N / A),
  • пользователь вводит текст, и вы предотвращаете его отображение (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • после того, как пользователь нажимает кнопку проверки, вы хотите очистить это поле (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • поскольку и новый, и старый vdom "", React не затрагивает dom.
Себастьян Лорбер
источник
1
Я реализовал версию keyPress, которая предупреждает текст при нажатии клавиши ввода. jsfiddle.net/kb3gN/11378
Лука Колоннелло
@LucaColonnello вам лучше использовать, {...this.props}чтобы клиент мог настроить это поведение извне,
Себастьен Лорбер,
1
Не могли бы вы объяснить, как shouldComponentUpdateкод предотвращает скачки курсора?
kmoe
1
@kmoe, потому что компонент никогда не обновляется, если в contentEditable уже есть соответствующий текст (например, при нажатии клавиши). Обновление contentEditable с помощью React приводит к скачку курсора. Попробуйте без contentEditable и убедитесь сами;)
Себастьян Лорбер,
1
Классная реализация. Похоже, у вашей ссылки JSFiddle есть проблема.
Дэниел говорит: "Восстановите Монику"
28

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

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Абхишек Кантхед
источник
3
Не нужно отрицать это, это работает! Просто не забудьте использовать, onInputкак указано в примере.
Себастьян Томас
Красиво и чисто, надеюсь, он работает на многих устройствах и в браузерах!
JulienRioux
8
Он постоянно перемещает курсор в начало текста, когда я обновляю текст с помощью состояния React.
Juntae
18

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

Когда editableprop есть false, я использую textprop как есть, но когда он есть 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;
Дан Абрамов
источник
Не могли бы вы подробнее рассказать о проблемах, которые у вас возникли, с другими ответами?
NVI
1
@NVI: мне нужна защита от инъекций, поэтому оставлять HTML как есть не вариант. Если я не помещаю HTML и не использую textContent, я получаю всевозможные несоответствия в браузере и не могу реализовать их shouldComponentUpdateтак легко, поэтому даже это больше не спасает меня от переходов курсора. Наконец, у меня есть :empty:beforeзаполнители псевдоэлементов CSS, но эта shouldComponentUpdateреализация помешала FF и Safari очистить поле, когда оно очищено пользователем. Мне потребовалось 5 часов, чтобы понять, что я могу обойти все эти проблемы с помощью неконтролируемого CE.
Дэн Абрамов
Я не совсем понимаю, как это работает. Вы никогда не меняются editableв UncontrolledContentEditable. Не могли бы вы предоставить работоспособный пример?
NVI
@NVI: Это немного сложно, так как я использую здесь внутренний модуль React .. В основном я устанавливаю editableизвне. Подумайте о поле, которое можно редактировать в строке, когда пользователь нажимает «Изменить», и которое должно быть снова доступно только для чтения, когда пользователь нажимает «Сохранить» или «Отмена». Поэтому, когда он доступен только для чтения, я использую реквизиты, но перестаю смотреть на них всякий раз, когда вхожу в «режим редактирования», и снова смотрю на реквизиты только после выхода из него.
Дэн Абрамов
3
Для тех, для кого вы собираетесь использовать этот код, React переименовал escapeTextForBrowserв escapeTextContentForBrowser.
выходной
8

Поскольку после завершения редактирования фокус элемента всегда теряется, вы можете просто использовать ловушку onBlur.

<div onBlur={(e)=>{console.log(e.currentTarget.textContent)}} contentEditable suppressContentEditableWarning={true}>
     <p>Lorem ipsum dolor.</p>
</div>
Saint Laurent
источник
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;
    }
}
Klugjo
источник
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
Автор предоставил мой SO-ответ в package.json. Это почти тот же код, который я опубликовал, и я подтверждаю, что этот код у меня работает. github.com/lovasoa/react-contenteditable/blob/master/…
Себастьян Лорбер