React.js: onChange-Ereignis für contentEditable

120

Wie höre ich, um ein Ereignis für eine contentEditablebasierte Steuerung zu ändern ?

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
quelle
11
Nachdem ich selbst damit zu kämpfen hatte und Probleme mit vorgeschlagenen Antworten hatte, beschloss ich, es stattdessen unkontrolliert zu machen. Das heißt, habe ich initialValuein stateund verwenden Sie es in render, aber ich lasse nicht aktualisiert es weiter reagieren.
Dan Abramov
Ihre JSFiddle funktioniert nicht
Green
Ich habe es vermieden, mit Problemen zu kämpfen, contentEditableindem ich meinen Ansatz geändert habe - anstelle eines spanoder paragraphhabe ich ein inputzusammen mit seinem readonlyAttribut verwendet.
ovidiu-miu

Antworten:

79

Bearbeiten: Siehe Sebastien Lorbers Antwort, die einen Fehler in meiner Implementierung behebt.


Verwenden Sie das Ereignis onInput und optional onBlur als Fallback. Möglicherweise möchten Sie den vorherigen Inhalt speichern, um das Senden zusätzlicher Ereignisse zu verhindern.

Ich persönlich hätte dies als meine Renderfunktion.

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

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

jsbin

Welches verwendet diesen einfachen Wrapper um 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;
    }
});
Räuber
quelle
1
@NVI, es ist die shouldComponentUpdate-Methode. Es wird nur springen, wenn die HTML-Requisite nicht mit dem tatsächlichen HTML-Code im Element synchronisiert ist. zB wenn du es getan hastthis.setState({html: "something not in the editable div"}})
Brigand
1
nett, aber ich denke, der Anruf zu this.getDOMNode().innerHTMLin shouldComponentUpdateist nicht sehr optimiert richtig
Sebastien Lorber
@SebastienLorber nicht sehr optimiert, aber ich bin mir ziemlich sicher, dass es besser ist, das HTML zu lesen, als es einzustellen. Die einzige andere Option, die mir in den Sinn kommt, ist das Abhören aller Ereignisse, die das HTML ändern könnten, und wenn diese auftreten, zwischenspeichern Sie das HTML. Das wäre wahrscheinlich die meiste Zeit schneller, würde aber viel Komplexität hinzufügen. Dies ist die sehr sichere und einfache Lösung.
Brigand
3
Dies ist tatsächlich ein wenig fehlerhaft, wenn Sie state.htmlden letzten "bekannten" Wert festlegen möchten. React aktualisiert das DOM nicht, da das neue HTML in Bezug auf React genau das gleiche ist (obwohl das tatsächliche DOM unterschiedlich ist). Siehe jsfiddle . Ich habe keine gute Lösung dafür gefunden, daher sind alle Ideen willkommen.
Universität
1
@dchest shouldComponentUpdatesollte rein sein (keine Nebenwirkungen haben).
Brigand
66

Bearbeiten Sie 2015

Jemand hat mit meiner Lösung ein NPM-Projekt erstellt: https://github.com/lovasoa/react-contenteditable

Bearbeiten 06/2016: Ich habe gerade ein neues Problem festgestellt, das auftritt, wenn der Browser versucht, das HTML, das Sie ihm gerade gegeben haben, neu zu formatieren, was dazu führt, dass die Komponente immer wieder neu gerendert wird. Sehen

Edit 07/2016: Hier ist mein Produktionsinhalt. Bearbeitbare Implementierung. Es gibt einige zusätzliche Optionen react-contenteditable, die Sie möglicherweise möchten, darunter:

  • Verriegelung
  • Imperative API zum Einbetten von HTML-Fragmenten
  • Fähigkeit, den Inhalt neu zu formatieren

Zusammenfassung:

Die Lösung von FakeRainBrigand hat für mich einige Zeit ganz gut funktioniert, bis ich neue Probleme bekam. ContentEditables sind ein Schmerz und nicht wirklich einfach zu handhaben ...

Diese JSFiddle demonstriert das Problem.

Wie Sie sehen können Clear, wird der Inhalt nicht gelöscht , wenn Sie einige Zeichen eingeben und auf klicken . Dies liegt daran, dass wir versuchen, den inhaltsbearbeitbaren Wert auf den letzten bekannten virtuellen Dom-Wert zurückzusetzen.

So scheint es, dass:

  • Sie müssen shouldComponentUpdateCaret-Positionssprünge verhindern
  • Sie können sich nicht auf den VDOM-Differenzierungsalgorithmus von React verlassen, wenn Sie shouldComponentUpdatediese Methode verwenden.

Sie benötigen also eine zusätzliche Zeile, damit Sie bei jeder shouldComponentUpdateRückgabe von yes sicher sind, dass der DOM-Inhalt tatsächlich aktualisiert wird.

Die Version hier fügt also ein hinzu componentDidUpdateund wird:

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

Der virtuelle Dom bleibt veraltet und es ist möglicherweise nicht der effizienteste Code, aber zumindest funktioniert er :) Mein Fehler ist behoben


Einzelheiten:

1) Wenn Sie shouldComponentUpdate setzen, um Caret-Sprünge zu vermeiden, wird die inhaltsbearbeitbare Datei nie erneut gerendert (zumindest bei Tastenanschlägen).

2) Wenn die Komponente bei einem Tastendruck nie erneut gerendert wird, behält React einen veralteten virtuellen Dom für diese inhaltsbearbeitbare Datei bei.

3) Wenn React eine veraltete Version von contenteditable in seinem virtuellen Dom-Baum beibehält, und wenn Sie versuchen, die contenteditable auf den Wert zurückzusetzen, der im virtuellen Dom veraltet ist, berechnet React während des virtuellen Dom-Diff, dass keine Änderungen an vorgenommen wurden bewerben Sie sich beim DOM!

Dies geschieht meistens, wenn:

  • Sie haben anfangs eine leere inhaltsbearbeitbare Datei (shouldComponentUpdate = true, prop = "", vorherige vdom = N / A),
  • Der Benutzer gibt Text ein und Sie verhindern das Rendern (shouldComponentUpdate = false, prop = text, previous vdom = "").
  • Nachdem der Benutzer auf eine Validierungsschaltfläche geklickt hat, möchten Sie dieses Feld leeren (shouldComponentUpdate = false, prop = "", previous vdom = "").
  • Da sowohl das neu produzierte als auch das alte Vdom "" sind, berührt React das Dom nicht.
Sebastien Lorber
quelle
1
Ich habe die keyPress-Version implementiert, die den Text benachrichtigt, wenn die Eingabetaste gedrückt wird. jsfiddle.net/kb3gN/11378
Luca Colonnello
@ LucaColonnello Sie sollten besser verwenden, {...this.props}damit der Client dieses Verhalten von außen anpassen kann
Sebastien Lorber
Oh ja, das ist besser! Ehrlich gesagt habe ich diese Lösung nur versucht, um zu überprüfen, ob das keyPress-Ereignis auf div funktioniert! Vielen Dank für Klarstellungen
Luca Colonnello
1
Können Sie erklären, wie der shouldComponentUpdateCode Caret-Sprünge verhindert?
kmoe
1
@kmoe, da die Komponente niemals aktualisiert wird, wenn contentEditable bereits über den entsprechenden Text verfügt (dh bei Tastendruck). Durch das Aktualisieren von contentEditable mit React springt das Caret. Versuchen Sie es ohne InhaltEditable und sehen Sie sich selbst;)
Sebastien Lorber
28

Dies ist die einfachste Lösung, die für mich funktioniert hat.

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Abhishek Kanthed
quelle
3
Keine Notwendigkeit, dies abzustimmen, es funktioniert! Denken Sie daran, onInputwie im Beispiel angegeben zu verwenden.
Sebastian Thomas
Schön und sauber, ich hoffe es funktioniert auf vielen Geräten und Browsern!
JulienRioux
6
Caret wird ständig an den Textanfang verschoben, wenn ich Text mit dem Status "Reagieren" aktualisiere.
Juntae
18

Dies ist wahrscheinlich nicht genau die Antwort, nach der Sie suchen, aber nachdem ich selbst damit zu kämpfen hatte und Probleme mit vorgeschlagenen Antworten hatte, beschloss ich, sie stattdessen unkontrolliert zu machen.

Wenn editableprop ist false, verwende ich textprop so wie es ist, aber wenn es ist true, wechsle ich in den Bearbeitungsmodus, in dem textes keine Auswirkungen hat (aber zumindest flippt der Browser nicht aus). Während dieser Zeit onChangewerden von der Steuerung ausgelöst. Wenn ich editablewieder zu wechsle false, füllt es HTML mit allem, was übergeben wurde 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
quelle
Könnten Sie die Probleme, die Sie mit den anderen Antworten hatten, näher erläutern?
NVI
1
@NVI: Ich brauche Sicherheit vor Injektion, daher ist es keine Option, HTML so wie es ist zu setzen. Wenn ich kein HTML einfüge und textContent verwende, bekomme ich alle möglichen Browser-Inkonsistenzen und kann sie nicht shouldComponentUpdateso einfach implementieren, sodass ich selbst nicht mehr vor Caret-Sprüngen gerettet werde. Schließlich habe ich CSS-Pseudoelement- :empty:beforePlatzhalter, aber diese shouldComponentUpdateImplementierung hat FF und Safari daran gehindert, das Feld zu bereinigen, wenn es vom Benutzer gelöscht wird. Ich habe 5 Stunden gebraucht, um festzustellen, dass ich all diese Probleme mit unkontrolliertem CE umgehen kann.
Dan Abramov
Ich verstehe nicht ganz, wie es funktioniert. Sie ändern nie editablein UncontrolledContentEditable. Könnten Sie ein lauffähiges Beispiel liefern?
NVI
@NVI: Es ist ein bisschen schwierig, da ich hier ein internes React-Modul verwende. Grundsätzlich setze ich editablevon außen. Stellen Sie sich ein Feld vor, das inline bearbeitet werden kann, wenn der Benutzer auf "Bearbeiten" drückt, und das nur dann schreibgeschützt sein sollte, wenn der Benutzer auf "Speichern" oder "Abbrechen" drückt. Wenn es schreibgeschützt ist, verwende ich Requisiten, aber ich höre auf, sie zu betrachten, wenn ich in den „Bearbeitungsmodus“ gehe, und schaue Requisiten erst wieder an, wenn ich sie verlasse.
Dan Abramov
3
Für wen Sie werden diesen Code verwenden, hat React umbenannt escapeTextForBrowserzu escapeTextContentForBrowser.
Wuct
8

Da nach Abschluss der Bearbeitung der Fokus des Elements immer verloren geht, können Sie einfach den onBlur-Hook verwenden.

<div onBlur={(e)=>{console.log(e.currentTarget.textContent)}} contentEditable suppressContentEditableWarning={true}>
     <p>Lorem ipsum dolor.</p>
</div>
Saint Laurent
quelle
5

Ich schlage vor, dazu einen mutationsObserver zu verwenden. Es gibt Ihnen viel mehr Kontrolle darüber, was los ist. Außerdem erfahren Sie mehr darüber, wie das Durchsuchen alle Tastenanschläge interpretiert

Hier in 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
quelle
2

Hier ist eine Komponente, die vieles davon von lovasoa enthält: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Er shims das Ereignis in der 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;
}

Ich verwende einen ähnlichen Ansatz erfolgreich

Jeff
quelle
1
Der Autor hat meine SO-Antwort in package.json gutgeschrieben. Dies ist fast derselbe Code, den ich gepostet habe, und ich bestätige, dass dieser Code für mich funktioniert. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber