React.js: evento onChange para contentEditable


120

¿Cómo escucho el cambio de evento para el contentEditablecontrol basado en?

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
Habiendo luchado con esto yo mismo y teniendo problemas con las respuestas sugeridas, decidí hacerlo sin control. Es decir, que puse initialValueen statey lo uso en render, pero no dejo Reaccionar actualización aún más.
Dan Abramov

Su JSFiddle no funciona
Verde

Evité luchar con el contentEditablecambio de mi enfoque, en lugar de un spano paragraph, he usado un inputjunto con su readonlyatributo.
ovidiu-miu

Respuestas:


79

Editar: vea la respuesta de Sebastien Lorber que corrige un error en mi implementación.


Utilice el evento onInput y, opcionalmente, onBlur como respaldo. Es posible que desee guardar el contenido anterior para evitar enviar eventos adicionales.

Personalmente tendría esto como mi función de renderizado.

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

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

jsbin

Que usa este simple contenedor alrededor de 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, es el método shouldComponentUpdate. Solo saltará si el html prop no está sincronizado con el html real en el elemento. por ejemplo, si lo hicistethis.setState({html: "something not in the editable div"}})
Brigand

1
agradable, pero supongo que la llamada a this.getDOMNode().innerHTMLen shouldComponentUpdateno está muy optimizado derecha
Sebastien Lorber

@SebastienLorber no está muy optimizado, pero estoy bastante seguro de que es mejor leer el html que configurarlo. La única otra opción en la que puedo pensar es escuchar todos los eventos que podrían cambiar el html, y cuando esos suceden, guardas en caché el html. Eso probablemente sería más rápido la mayor parte del tiempo, pero agrega mucha complejidad. Esta es la solución muy segura y sencilla.
Brigand

3
En realidad, esto es un poco defectuoso cuando desea establecer state.htmlel último valor "conocido", React no actualizará el DOM porque el nuevo html es exactamente el mismo en lo que respecta a React (aunque el DOM real es diferente). Consulte jsfiddle . No he encontrado una buena solución para esto, por lo que cualquier idea es bienvenida.
univerio

1
@dchest shouldComponentUpdatedebe ser puro (no tener efectos secundarios).
Brigand

66

Editar 2015

Alguien ha hecho un proyecto en NPM con mi solución: https://github.com/lovasoa/react-contenteditable

Edición 06/2016: Acabo de encontrar un nuevo problema que ocurre cuando el navegador intenta "reformatear" el html que le acaba de dar, lo que lleva a que el componente siempre vuelva a renderizarse. Ver

Edición 07/2016: aquí está mi contenido de producción Implementación editable. Tiene algunas opciones adicionales sobre las react-contenteditableque quizás desee, que incluyen:

  • cierre
  • API imperativa que permite incrustar fragmentos html
  • capacidad para reformatear el contenido

Resumen:

La solución de FakeRainBrigand me ha funcionado bastante bien durante algún tiempo hasta que tuve nuevos problemas. ContentEditables son una molestia, y no son realmente fáciles de manejar con React ...

Este JSFiddle demuestra el problema.

Como puede ver, cuando escribe algunos caracteres y hace clic en Clear , el contenido no se borra. Esto se debe a que intentamos restablecer el contenteditable al último valor de dom virtual conocido.

Entonces parece que:

  • Debes shouldComponentUpdateevitar los saltos de posición del cursor
  • No puede confiar en el algoritmo de diferenciación VDOM de React si lo usa de shouldComponentUpdateesta manera.

Por lo tanto, necesita una línea adicional para que, siempre que shouldComponentUpdatedevuelva sí, esté seguro de que el contenido DOM está realmente actualizado.

Entonces, la versión aquí agrega ay se componentDidUpdateconvierte en:

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

El dom virtual permanece desactualizado y puede que no sea el código más eficiente, pero al menos funciona :) Mi error está resuelto


Detalles:

1) Si pones shouldComponentUpdate para evitar saltos de intercalación, entonces el contenteditable nunca se vuelve a reproducir (al menos en las pulsaciones de teclas)

2) Si el componente nunca se vuelve a reproducir con la pulsación de una tecla, React mantiene un dom virtual obsoleto para este contenteditable.

3) Si React mantiene una versión desactualizada de contenteditable en su árbol de dom virtual, entonces si intenta restablecer el contenteditable al valor desactualizado en el dom virtual, entonces durante la diferencia de dom virtual, React calculará que no hay cambios en aplicar al DOM!

Esto sucede principalmente cuando:

  • tiene un contenteditable vacío inicialmente (shouldComponentUpdate = true, prop = "", vdom anterior = N / A),
  • el usuario escribe algo de texto y evita las representaciones (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • después de que el usuario hace clic en un botón de validación, desea vaciar ese campo (shouldComponentUpdate = false, prop = "", vdom anterior = "")
  • como tanto el vdom recién producido como el antiguo son "", React no toca el dom.

1
Implementé la versión keyPress que alerta al texto cuando se presiona la tecla Enter. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello es mejor {...this.props}que lo uses para que el cliente pueda personalizar este comportamiento desde afuera
Sebastien Lorber

¡Oh, sí, esto es mejor! Honestamente, probé esta solución solo para verificar si el evento keyPress funciona en div. Gracias por las aclaraciones
Luca Colonnello

1
¿Podría explicar cómo el shouldComponentUpdatecódigo previene los saltos de intercalación?
kmoe

1
@kmoe porque el componente nunca se actualiza si el contentEditable ya tiene el texto apropiado (es decir, al presionar una tecla). Actualizar el contenido editable con React hace que el cursor salte. Prueba sin contenido Editable y mírate a ti mismo;)
Sebastien Lorber

28

Esta es la solución más simple que funcionó para mí.

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

3
No hay necesidad de rechazar esto, ¡funciona! Solo recuerde usarlo onInputcomo se indica en el ejemplo.
Sebastian Thomas

Agradable y limpio, espero que funcione en muchos dispositivos y navegadores.
JulienRioux

7
Mueve el cursor al principio del texto constantemente cuando actualizo el texto con el estado React.
Juntae

18

Probablemente esta no sea exactamente la respuesta que está buscando, pero habiendo luchado con esto yo mismo y teniendo problemas con las respuestas sugeridas, decidí hacerlo sin control.

Cuando editablees prop false, utilizo textprop tal como está, pero cuando lo está true, cambio al modo de edición en el que textno tiene ningún efecto (pero al menos el navegador no se asusta). Durante este tiempo onChangeson disparados por el control. Finalmente, cuando vuelvo a cambiar editablea false, llena HTML con lo que se pasó 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;

¿Podría ampliar los problemas que tenía con las otras respuestas?
NVI

1
@NVI: Necesito seguridad contra la inyección, por lo que poner HTML como está no es una opción. Si no pongo HTML y uso textContent, obtengo todo tipo de inconsistencias en el navegador y no puedo implementarlo shouldComponentUpdatetan fácilmente, por lo que ya no me salva de los saltos de intercalación. Finalmente, tengo :empty:beforemarcadores de posición de pseudoelementos CSS, pero esta shouldComponentUpdateimplementación impidió que FF y Safari limpiaran el campo cuando el usuario lo borraba. Tardé 5 horas en darme cuenta de que puedo eludir todos estos problemas con la CE incontrolada.
Dan Abramov

No entiendo muy bien cómo funciona. Nunca cambia editableen UncontrolledContentEditable. ¿Podría proporcionar un ejemplo ejecutable?
NVI

@NVI: Es un poco difícil ya que uso un módulo interno de React aquí ... Básicamente lo configuro editabledesde afuera. Piense en un campo que se puede editar en línea cuando el usuario presiona "Editar" y debería ser de nuevo de solo lectura cuando el usuario presiona "Guardar" o "Cancelar". Entonces, cuando es de solo lectura, uso accesorios, pero dejo de mirarlos cada vez que entro en el "modo de edición" y solo miro los accesorios nuevamente cuando salgo de él.
Dan Abramov

3
Para quién va a usar este código, React ha cambiado escapeTextForBrowserde nombre a escapeTextContentForBrowser.
wuct

8

Dado que cuando se completa la edición, el enfoque del elemento siempre se pierde, simplemente puede usar el gancho onBlur.

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

5

Sugiero usar un mutationObserver para hacer esto. Te da mucho más control sobre lo que está sucediendo. También le brinda más detalles sobre cómo el navegador interpreta todas las pulsaciones de teclas

Aquí en 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

Aquí hay un componente que incorpora mucho de esto por lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Calza el evento en el emisChange

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

Estoy usando un enfoque similar con éxito


1
El autor ha acreditado mi respuesta SO en package.json. Este es casi el mismo código que publiqué y confirmo que este código funciona para mí. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.