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/


11
Сам боровся з цим і маючи проблеми із запропонованими відповідями, я вирішив зробити це безконтрольним. Тобто, я поклав initialValueв stateі використовувати його в render, але я не дозволяю Реагувати поновлення його далі.
Дан Абрамов

Ваш JSFiddle не працює
Зелений

Я уникнув боротьби з contentEditableцим, змінивши свій підхід - замість spanабо paragraphя використав атрибут inputразом із його readonly.
ovidiu-miu

Відповіді:


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"}})
Бриганд

1
добре , але я припускаю , що виклик this.getDOMNode().innerHTMLв shouldComponentUpdateне надто оптимізований право
Себастьєн Лорбер

@SebastienLorber не дуже оптимізований, але я впевнений, що краще прочитати html, ніж встановити його. Єдиний інший варіант, про який я можу придумати, - це прослуховування всіх подій, які можуть змінити html, і коли вони трапляються, ви кешуєте html. Це, мабуть, буде швидше більшу частину часу, але додасть багато складності. Це дуже впевнене і просте рішення.
Бриганд

3
Це насправді злегка хибно, коли ви хочете встановити state.htmlостаннє "відоме" значення, React не буде оновлювати DOM, оскільки новий html точно такий же, як і для React (навіть якщо фактичний DOM відрізняється). Дивіться jsfiddle . Я не знайшов для цього хорошого рішення, тому будь-які ідеї вітаються.
univerio

1
@dchest shouldComponentUpdateмає бути чистим (не мати побічних ефектів).
Бриганд

66

Редагувати 2015 рік

Хтось зробив проект щодо NPM зі своїм рішенням: https://github.com/lovasoa/react-contenteditable

Редагувати 06/2016: Щойно я спричинив нову проблему, яка виникає, коли браузер намагається «переформатувати» той HTML, який ви йому щойно надали, що призводить до того, що компонент завжди буде повторно рендерінг. Побачити

Редагувати 07/2016: ось мій виробничий контент Придатне втілення. У ньому є кілька додаткових варіантів, react-contenteditableякі ви хочете, зокрема:

  • блокування
  • імперативний API, що дозволяє вставляти HTML-фрагменти
  • можливість переформатувати зміст

Підсумок:

Рішення FakeRainBrigand деякий час працювало для мене досить добре, поки у мене не виникли нові проблеми. ContentEditables - це біль, і не дуже легко впоратися з React ...

Цей JSFiddle демонструє проблему.

Як бачите, коли ви вводите деякі символи та натискаєте на Clear, вміст не очищається. Це тому, що ми намагаємось відновити змістовне значення до останнього відомого значення віртуального дому.

Тож здається, що:

  • Вам потрібно shouldComponentUpdateзапобігти стрибкам позиції карет
  • Якщо ви використовуєте shouldComponentUpdateцей спосіб, ви не можете покластися на алгоритм VDOM, що відрізняється від React .

Тому вам потрібен додатковий рядок, щоб кожен раз, коли 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, щоб уникнути стрибків карет, то змістовна відповідь ніколи не повторюється (принаймні на натискання клавіш)

2) Якщо компонент ніколи не видається на ключовому обведенні, тоді React зберігає застарілий віртуальний дом для цього вмісту.

3) Якщо React зберігає застарілу версію contenteditable у своєму віртуальному дереві dom, то якщо ви спробуєте відновити contenteditable до значення, застарілого у віртуальному dom, то під час віртуального dom diff, React обчислить, що ніяких змін у зверніться до DOM!

Це відбувається в основному, коли:

  • у вас спочатку порожній контент-придатний (повиненComponentUpdate = true, prop = "", попередній vdom = N / A),
  • користувач вводить якийсь текст, і ви запобігаєте візуалізації (shouldComponentUpdate = false, prop = text, попередній vdom = "")
  • після того, як користувач натисне кнопку підтвердження, ви бажаєте спорожнити це поле (shouldComponentUpdate = false, prop = "", попередній vdom = "")
  • як і щойно створений, і старий vdom є "", React не торкається дому.

1
Я реалізував версію keyPress, яка сповіщає про текст після натискання клавіші введення. jsfiddle.net/kb3gN/11378
Лука Колоннелло

@LucaColonnello вам краще скористатися, {...this.props}щоб клієнт міг налаштувати цю поведінку ззовні
Себастьян Лорбер

О так, це краще! Чесно кажучи, я спробував це рішення лише для того, щоб перевірити, чи працює keyPress подія на діві! Дякую за роз’яснення
Лука Колоннелло

1
Чи можете ви пояснити, як shouldComponentUpdateкод запобігає стрибкам карет?
kmoe

1
@kmoe, тому що компонент ніколи не оновлюється, якщо contentEditable вже має відповідний текст (тобто на натисканні клавіші). Оновлення contentEditable за допомогою React робить стрибок карети. Спробуйте без вмісту редагувати і побачити себе;)
Себастьян Лорбер

28

Це найпростіше рішення, яке працювало для мене.

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

3
Не потрібно це зволікати, це працює! Просто не забудьте використовувати, onInputяк зазначено в прикладі.
Себастьян Томас

Приємно і чисто, сподіваюся, він працює на багатьох пристроях та браузерах!
JulienRioux

8
Коли я оновлюю текст у режимі React, він постійно переміщує карету до початку тексту.
Джунта

18

Це, мабуть, не зовсім відповідь, яку ви шукаєте, але, маючи проблеми з цим сам і маючи проблеми із запропонованими відповідями, я вирішив зробити це непідконтрольним.

Якщо editableє опора false, я використовую textпідтримку такою, якою є, але коли вона є 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 годин, щоб зрозуміти, що я можу усунути всі ці проблеми з неконтрольованим СЕ.
Дан Абрамов

Я не зовсім розумію, як це працює. Ви ніколи не змінюються 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>

5

Я пропоную використовувати мутаціюObserver для цього. Це дає вам набагато більше контролю над тим, що відбувається. Він також дає більше детальних відомостей про те, як перегляд інтерпретує всі натискання клавіш

Тут, у 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;
    }
}

2

Ось компонент, який містить велику частину цього сайту lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Він переливається подією в емітеЗміни

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/…
Себастьєн Лорбер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.