React.js: evento onChange per contentEditable


120

Come ascolto l'evento di cambiamento per il contentEditablecontrollo basato su?

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
Avendo lottato da solo con questo e avendo problemi con le risposte suggerite, ho deciso invece di renderlo incontrollato. Cioè, ho messo initialValuein statee usarlo in render, ma non lascio Reagire aggiornamento ulteriormente.
Dan Abramov

Il tuo JSFiddle non funziona
Verde

Ho evitato di lottare contentEditablecambiando il mio approccio - invece di un spano paragraph, ho usato un inputinsieme al suo readonlyattributo.
ovidiu-miu

Risposte:


79

Modifica: vedi la risposta di Sebastien Lorber che corregge un bug nella mia implementazione.


Usa l'evento onInput e, facoltativamente, onBlur come fallback. Potresti voler salvare i contenuti precedenti per impedire l'invio di eventi extra.

Personalmente lo avrei come funzione di rendering.

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

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

jsbin

Che utilizza questo semplice wrapper attorno a 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, è il metodo shouldComponentUpdate. Salterà solo se il prop html non è sincronizzato con l'html effettivo nell'elemento. ad esempio, se l'hai fattothis.setState({html: "something not in the editable div"}})
Brigand

1
bello ma immagino che la chiamata a this.getDOMNode().innerHTMLin shouldComponentUpdatenon sia molto ottimizzata a destra
Sebastien Lorber

@SebastienLorber non è molto ottimizzato, ma sono abbastanza sicuro che sia meglio leggere l'html, piuttosto che impostarlo. L'unica altra opzione a cui riesco a pensare è ascoltare tutti gli eventi che potrebbero cambiare l'html, e quando si verificano si memorizza nella cache l'html. Probabilmente sarebbe più veloce la maggior parte del tempo, ma aggiunge molta complessità. Questa è la soluzione molto sicura e semplice.
Brigante

3
Questo in realtà è leggermente imperfetto quando si desidera impostare state.htmll'ultimo valore "noto", React non aggiornerà il DOM perché il nuovo html è esattamente lo stesso per quanto riguarda React (anche se il DOM effettivo è diverso). Vedi jsfiddle . Non ho trovato una buona soluzione per questo, quindi qualsiasi idea è benvenuta.
univerio

1
@dchest shouldComponentUpdatedovrebbe essere puro (non avere effetti collaterali).
Brigante

66

Modifica 2015

Qualcuno ha realizzato un progetto su NPM con la mia soluzione: https://github.com/lovasoa/react-contenteditable

Modifica 06/2016: ho appena riscontrato un nuovo problema che si verifica quando il browser cerca di "riformattare" l'html che gli hai appena fornito, portando al componente sempre il rendering. Vedere

Modifica 07/2016: ecco il mio contenuto di produzione Implementazione modificabile. Ha alcune opzioni aggiuntive rispetto a react-contenteditablequelle che potresti desiderare, tra cui:

  • bloccaggio
  • API imperativa che consente di incorporare frammenti html
  • capacità di riformattare il contenuto

Sommario:

La soluzione di FakeRainBrigand ha funzionato abbastanza bene per me per un po 'di tempo fino a quando non ho avuto nuovi problemi. ContentEditables sono una seccatura e non sono davvero facili da gestire con React ...

Questo JSFiddle dimostra il problema.

Come puoi vedere, quando digiti alcuni caratteri e fai clic su Clear , il contenuto non viene cancellato. Questo perché proviamo a ripristinare il contenteditable all'ultimo valore di dom virtuale noto.

Quindi sembra che:

  • Hai bisogno shouldComponentUpdate evitare salti di posizione del cursore
  • Non puoi fare affidamento sull'algoritmo di diffing VDOM di React se usi shouldComponentUpdatequesto modo.

Quindi hai bisogno di una riga in più in modo che ogni volta shouldComponentUpdate restituisce sì, sei sicuro che il contenuto del DOM sia effettivamente aggiornato.

Quindi la versione qui aggiunge un componentDidUpdatee diventa:

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

Il dominio virtuale rimane obsoleto e potrebbe non essere il codice più efficiente, ma almeno funziona :) Il mio bug è stato risolto


Dettagli:

1) Se si inserisce shouldComponentUpdate per evitare salti di accento circonflesso, l'elemento contenteditable non viene mai ripetuto (almeno quando si preme il tasto)

2) Se il componente non viene mai riprodotto nuovamente alla pressione del tasto, React mantiene un dominio virtuale obsoleto per questo contenuto modificabile.

3) Se React mantiene una versione obsoleta di contenteditable nel suo albero dom virtuale, se provi a reimpostare contenteditable al valore obsoleto nel dominio virtuale, durante il differenziale dom virtuale, React calcolerà che non ci sono modifiche a applicare al DOM!

Ciò accade principalmente quando:

  • hai inizialmente un contenteditable vuoto (shouldComponentUpdate = true, prop = "", previous vdom = N / A),
  • l'utente digita del testo e si impedisce il rendering (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • dopo che l'utente fa clic su un pulsante di convalida, si desidera svuotare il campo (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • poiché sia ​​il nuovo vdom che il vecchio sono "", React non tocca il dom.

1
Ho implementato la versione keyPress che avvisa il testo quando viene premuto il tasto Invio. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello faresti meglio a usare in {...this.props}modo che il cliente possa personalizzare questo comportamento dall'esterno
Sebastien Lorber

Oh sì, questo è meglio! Onestamente ho provato questa soluzione solo per verificare se l'evento keyPress funziona su div! Grazie per chiarimenti
Luca Colonnello

1
Puoi spiegare come il shouldComponentUpdatecodice previene i salti con il cursore?
kmoe

1
@kmoe perché il componente non si aggiorna mai se contentEditable ha già il testo appropriato (cioè alla pressione di un tasto). L'aggiornamento del contenuto Modificabile con React fa saltare il cursore. Prova senza contenuto Modificabile e vedi te stesso;)
Sebastien Lorber

28

Questa è la soluzione più semplice che ha funzionato per me.

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

3
Non c'è bisogno di downvote questo, funziona! Ricorda solo di usare onInputcome indicato nell'esempio.
Sebastian Thomas,

Bello e pulito, spero che funzioni su molti dispositivi e browser!
JulienRioux

7
Sposta costantemente il cursore all'inizio del testo quando aggiorno il testo con lo stato React.
Juntae

18

Questa probabilmente non è esattamente la risposta che stai cercando, ma avendo lottato da solo con questo e avendo problemi con le risposte suggerite, ho deciso di renderlo incontrollato.

Quando editableprop è false, io uso textprop così com'è, ma quando lo è true, passo alla modalità di modifica in cui textnon ha alcun effetto (ma almeno il browser non va fuori di testa). Durante questo periodo onChangevengono attivati ​​dal controllo. Infine, quando editabletorno a false, riempie l'HTML con tutto ciò che è stato passato 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;

Potresti approfondire i problemi che stavi avendo con le altre risposte?
NVI

1
@ NVI: ho bisogno di sicurezza dall'iniezione, quindi inserire HTML così com'è non è un'opzione. Se non inserisco HTML e utilizzo textContent, ottengo tutti i tipi di incoerenze del browser e non riesco a implementarlo shouldComponentUpdatecosì facilmente, quindi anche se non mi salta più dai salti con il cursore. Infine, ho dei :empty:beforesegnaposto di pseudo-elementi CSS, ma questa shouldComponentUpdateimplementazione ha impedito a FF e Safari di ripulire il campo quando è stato cancellato dall'utente. Mi ci sono volute 5 ore per capire che posso eludere tutti questi problemi con CE incontrollata.
Dan Abramov

Non capisco bene come funziona. Non cambi mai editabledentro UncontrolledContentEditable. Potresti fornire un esempio eseguibile?
NVI

@ NVI: È un po 'difficile dato che qui uso un modulo interno React .. Fondamentalmente ho impostato editabledall'esterno. Pensa a un campo che può essere modificato in linea quando l'utente preme "Modifica" e dovrebbe essere di nuovo di sola lettura quando l'utente preme "Salva" o "Annulla". Quindi, quando è di sola lettura, utilizzo gli oggetti di scena, ma smetto di guardarli ogni volta che entro in "modalità di modifica" e guardo di nuovo gli oggetti di scena solo quando esco.
Dan Abramov

3
Per chi si sta per utilizzare questo codice, Reagire ha rinominato escapeTextForBrowsera escapeTextContentForBrowser.
wuct

8

Poiché quando la modifica è completa, il focus dell'elemento viene sempre perso, puoi semplicemente usare l'hook onBlur.

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

5

Suggerisco di utilizzare un mutationObserver per farlo. Ti dà molto più controllo su cosa sta succedendo. Fornisce inoltre maggiori dettagli su come la navigazione interpreta tutte le sequenze di tasti

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

2

Ecco un componente che incorpora molto di questo da lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Sposta l'evento in 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;
}

Sto usando con successo un approccio simile


1
L'autore ha accreditato la mia risposta SO in package.json. Questo è quasi lo stesso codice che ho pubblicato e confermo che questo codice funziona per me. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.