React.js: zdarzenie onChange dla contentEditable

120

Jak słuchać zdarzenia zmiany dla contentEditablesterowania opartego na bazie danych?

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
źródło
11
Po tym, jak sam się z tym zmagałem i miałem problemy z sugerowanymi odpowiedziami, postanowiłem zamiast tego zrobić to niekontrolowane. Oznacza to, że umieszczam initialValuego statei używam render, ale nie pozwalam, aby React go dalej aktualizował.
Dan Abramov
Twój JSFiddle nie działa
Green
Uniknąłem zmagań contentEditable, zmieniając swoje podejście - zamiast spanlub paragraphużyłem znaku inputwraz z jego readonlyatrybutem.
ovidiu-miu

Odpowiedzi:

79

Edycja: Zobacz odpowiedź Sebastiena Lorbera, która naprawia błąd w mojej implementacji.


Użyj zdarzenia onInput i opcjonalnie onBlur jako rezerwy. Możesz chcieć zapisać poprzednią zawartość, aby zapobiec wysyłaniu dodatkowych zdarzeń.

Osobiście miałbym to jako moją funkcję renderującą.

var handleChange = function(event){
    this.setState({html: event.target.value});
}.bind(this);

return (<ContentEditable html={this.state.html} onChange={handleChange} />);

jsbin

Który używa tego prostego opakowania wokół 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;
    }
});
Bandyta
źródło
1
@NVI, to metoda shouldComponentUpdate. Przeskakuje tylko wtedy, gdy właściwość html nie jest zsynchronizowana z rzeczywistym kodem HTML w elemencie. np. jeśli takthis.setState({html: "something not in the editable div"}})
Brigand
1
fajnie, ale myślę, że wezwanie do this.getDOMNode().innerHTMLin shouldComponentUpdatenie jest zbyt dobrze zoptymalizowane
Sebastien Lorber
@SebastienLorber niezbyt zoptymalizowany, ale jestem pewien, że lepiej jest czytać kod HTML, niż go ustawiać. Jedyną inną opcją, o której przychodzi mi do głowy, jest odsłuchiwanie wszystkich zdarzeń, które mogą zmienić kod HTML, a kiedy to się stanie, buforujesz kod HTML. To prawdopodobnie byłoby szybsze przez większość czasu, ale dodaje dużo złożoności. To bardzo pewne i proste rozwiązanie.
Brigand
3
Jest to właściwie trochę błędne, gdy chcesz ustawić state.htmlostatnią „znaną” wartość, React nie zaktualizuje DOM, ponieważ nowy html jest dokładnie taki sam, jeśli chodzi o Reacta (nawet jeśli rzeczywisty DOM jest inny). Zobacz jsfiddle . Nie znalazłem na to dobrego rozwiązania, więc wszelkie pomysły są mile widziane.
univerio
1
@dchest shouldComponentUpdatepowinno być czyste (bez efektów ubocznych).
Brigand
66

Edytuj 2015

Ktoś wykonał projekt na NPM z moim rozwiązaniem: https://github.com/lovasoa/react-contenteditable

Edycja 06/2016: Właśnie napotkałem nowy problem, który pojawia się, gdy przeglądarka próbuje „przeformatować” kod HTML, który właśnie mu przekazałeś, co prowadzi do tego, że komponent zawsze renderuje się ponownie. Widzieć

Edycja 07/2016: oto moja zawartość produkcyjna Edytowalna implementacja. Ma kilka dodatkowych opcji, react-contenteditablektóre możesz chcieć, w tym:

  • zamykający
  • imperatywne API pozwalające na osadzanie fragmentów html
  • możliwość ponownego formatowania treści

Podsumowanie:

Rozwiązanie FakeRainBrigand działało dla mnie całkiem dobrze przez jakiś czas, dopóki nie pojawiły się nowe problemy. ContentEditables są uciążliwe i nie są łatwe w obsłudze React ...

Ten JSFiddle demonstruje problem.

Jak widać, po wpisaniu niektórych znaków i kliknięciu Clearzawartość nie jest czyszczona. Dzieje się tak, ponieważ próbujemy zresetować zawartość edytowalną do ostatniej znanej wartości wirtualnej domeny.

Więc wydaje się, że:

  • Musisz shouldComponentUpdatezapobiegać skokom pozycji karetki
  • Nie możesz polegać na algorytmie porównywania VDOM firmy React, jeśli używasz w shouldComponentUpdateten sposób.

Potrzebujesz więc dodatkowej linii, aby zawsze, gdy shouldComponentUpdatezwróci wartość tak, masz pewność, że zawartość DOM jest rzeczywiście zaktualizowana.

Tak więc wersja tutaj dodaje a componentDidUpdatei staje się:

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;
    }
});

Wirtualny dom pozostaje nieaktualny i może nie jest to najbardziej wydajny kod, ale przynajmniej działa :) Mój błąd został rozwiązany


Detale:

1) Jeśli umieścisz shouldComponentUpdate, aby uniknąć skoków karetki, treść edytowalna nigdy nie będzie ponownie renderowana (przynajmniej po naciśnięciu klawisza)

2) Jeśli komponent nigdy nie jest ponownie renderowany po naciśnięciu klawisza, React przechowuje nieaktualną wirtualną domenę dla tej zawartości, którą można edytować.

3) Jeśli React zachowuje przestarzałą wersję edytowalnej treści w swoim wirtualnym drzewie dom, to jeśli spróbujesz zresetować zawartość edytowalną do wartości nieaktualnej w wirtualnym dom, to podczas wirtualnego różnic dom, React obliczy, że nie ma żadnych zmian zastosuj do DOM!

Dzieje się tak głównie wtedy, gdy:

  • masz początkowo pustą treść do edycji (shouldComponentUpdate = true, prop = "", previous vdom = N / A),
  • użytkownik wpisuje tekst, a ty zapobiegasz renderowaniu (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • gdy użytkownik kliknie przycisk walidacji, chcesz opróżnić to pole (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • ponieważ zarówno nowo utworzone, jak i stare pliki vdom są "", React nie dotyka domeny.
Sebastien Lorber
źródło
1
Zaimplementowałem wersję keyPress, która ostrzega tekst po naciśnięciu klawisza Enter. jsfiddle.net/kb3gN/11378
Luca Colonnello
@LucaColonnello lepiej użyj, {...this.props}aby klient mógł dostosować to zachowanie z zewnątrz
Sebastien Lorber
O tak, to jest lepsze! Szczerze mówiąc, wypróbowałem to rozwiązanie tylko po to, aby sprawdzić, czy zdarzenie keyPress działa na div! Dzięki za wyjaśnienia
Luca Colonnello
1
Czy możesz wyjaśnić, w jaki sposób shouldComponentUpdatekod zapobiega skokom karetki?
kmoe
1
@kmoe, ponieważ komponent nigdy nie aktualizuje się, jeśli contentEditable ma już odpowiedni tekst (tj. po naciśnięciu klawisza). Aktualizacja treściEditable za pomocą React powoduje skok kursora. Spróbuj bez treściEdytuj i zobacz siebie;)
Sebastien Lorber
28

To jest najprostsze rozwiązanie, które zadziałało.

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Abhishek Kanthed
źródło
3
Nie musisz tego negatywnie oceniać, to działa! Pamiętaj tylko, aby używać onInputjak podano w przykładzie.
Sebastian Thomas
Ładny i czysty, mam nadzieję, że działa na wielu urządzeniach i przeglądarkach!
JulienRioux
8
Ciągle przesuwa kursor na początek tekstu, gdy aktualizuję tekst w stanie React.
Juntae
18

Prawdopodobnie nie jest to dokładnie odpowiedź, której szukasz, ale po tym, jak sam się z tym zmagałem i miałem problemy z sugerowanymi odpowiedziami, postanowiłem zamiast tego uczynić ją niekontrolowaną.

Kiedy editableprop jest false, używam textrekwizytu bez zmian, ale kiedy jest true, przełączam się na tryb edycji, w którym textnie ma żadnego efektu (ale przynajmniej przeglądarka nie wariuje). W tym czasie onChangesą odpalane przez sterowanie. Wreszcie, kiedy editablewracam do false, wypełnia HTML tym, co zostało przekazane 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;
Dan Abramov
źródło
Czy mógłbyś rozwinąć problemy, które miałeś z innymi odpowiedziami?
NVI
1
@NVI: Potrzebuję bezpieczeństwa przed wstrzyknięciem, więc umieszczanie HTML-a tak, jak jest, nie wchodzi w grę. Jeśli nie umieszczę HTML i nie użyję textContent, dostaję różnego rodzaju niespójności w przeglądarce i nie mogę shouldComponentUpdatetak łatwo zaimplementować , więc nawet to nie chroni mnie już przed skokami karetki. Na koniec mam :empty:beforesymbole zastępcze pseudoelementów CSS, ale ta shouldComponentUpdateimplementacja uniemożliwiła FF i Safari wyczyszczenie pola, gdy zostało wyczyszczone przez użytkownika. Pięć godzin zajęło mi uświadomienie sobie, że mogę ominąć te wszystkie problemy dzięki niekontrolowanemu CE.
Dan Abramov
Nie bardzo rozumiem, jak to działa. Nigdy nie zmienia editablesię UncontrolledContentEditable. Czy możesz podać działający przykład?
NVI
@NVI: To trochę trudne, ponieważ używam tutaj wewnętrznego modułu React .. Zasadniczo ustawiam editablez zewnątrz. Pomyśl o polu, które można edytować bezpośrednio, gdy użytkownik naciśnie „Edytuj” i powinno być ponownie odczytywane tylko wtedy, gdy użytkownik naciśnie „Zapisz” lub „Anuluj”. Więc kiedy jest tylko do odczytu, używam rekwizytów, ale przestaję na nie patrzeć za każdym razem, gdy wchodzę w „tryb edycji” i patrzę na rekwizyty dopiero po wyjściu z niego.
Dan Abramov
3
Dla kogo zamierzasz używać tego kodu, React zmienił nazwę escapeTextForBrowserna escapeTextContentForBrowser.
wuct
8

Ponieważ po zakończeniu edycji fokus elementu jest zawsze tracony, możesz po prostu użyć haka onBlur.

<div onBlur={(e)=>{console.log(e.currentTarget.textContent)}} contentEditable suppressContentEditableWarning={true}>
     <p>Lorem ipsum dolor.</p>
</div>
Saint Laurent
źródło
5

W tym celu sugeruję użycie mutationObserver. Daje dużo większą kontrolę nad tym, co się dzieje. Zawiera również więcej szczegółów na temat sposobu, w jaki przeglądarka interpretuje wszystkie naciśnięcia klawiszy

Tutaj w 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
źródło
2

Oto komponent, który zawiera wiele z tego przez lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Zmienia zdarzenie w 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;
}

Z powodzeniem stosuję podobne podejście

Jeff
źródło
1
Autor umieścił moją odpowiedź SO w pliku package.json. To jest prawie ten sam kod, który opublikowałem i potwierdzam, że działa dla mnie. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber