React.js: evento onChange para contentEditable

120

Como faço para ouvir o contentEditablecontrole baseado em evento de mudança ?

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
fonte
11
Tendo lutado contra isso sozinho e tendo problemas com as respostas sugeridas, decidi torná-lo descontrolado. Ou seja, eu coloquei initialValueem statee usá-lo em render, mas eu não deixo Reagir atualização ainda mais.
Dan Abramov,
Seu JSFiddle não funciona
Verde
Evitei lutar contentEditablemudando minha abordagem - em vez de um spanou paragraph, usei um inputjunto com seu readonlyatributo.
ovidiu-miu

Respostas:

79

Edit: Veja a resposta de Sebastien Lorber que corrige um bug em minha implementação.


Use o evento onInput e, opcionalmente, onBlur como um fallback. Você pode querer salvar o conteúdo anterior para evitar o envio de eventos extras.

Eu pessoalmente teria isso como minha função de renderização.

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

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

jsbin

Que usa este invólucro simples em torno 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;
    }
});
Brigand
fonte
1
@NVI, é o método shouldComponentUpdate. Ele só pulará se o prop html estiver fora de sincronia com o html real no elemento. por exemplo, se você fezthis.setState({html: "something not in the editable div"}})
Brigand
1
bom, mas eu acho que a chamada para this.getDOMNode().innerHTMLnos shouldComponentUpdatenão é muito otimizado direito
Sebastien Lorber
@SebastienLorber não está muito otimizado, mas tenho certeza que é melhor ler o html do que configurá-lo. A única outra opção que posso pensar é ouvir todos os eventos que podem alterar o html e, quando isso acontecer, você armazena o html em cache. Isso provavelmente seria mais rápido na maioria das vezes, mas adiciona muita complexidade. Esta é a solução muito simples e segura.
Brigand
3
Na verdade, isso é um pouco incorreto quando você deseja definir state.htmlo último valor "conhecido". O React não atualizará o DOM porque o novo html é exatamente o mesmo no que diz respeito ao React (embora o DOM real seja diferente). Veja jsfiddle . Não encontrei uma boa solução para isso, então qualquer ideia é bem-vinda.
univerio
1
@dchest shouldComponentUpdatedeve ser puro (não tem efeitos colaterais).
Brigand
66

Editar 2015

Alguém fez um projeto no NPM com minha solução: https://github.com/lovasoa/react-contenteditable

Editar 06/2016: Acabei de encontrar um novo problema que ocorre quando o navegador tenta "reformatar" o html que você acabou de fornecer a ele, levando a um componente sempre re-renderizado. Vejo

Editar 07/2016: aqui está minha implementação de produção de contentEditable. Ele tem algumas opções adicionais react-contenteditableque você pode desejar, incluindo:

  • trancando
  • API imperativa que permite incorporar fragmentos html
  • capacidade de reformatar o conteúdo

Resumo:

A solução do FakeRainBrigand funcionou muito bem para mim por algum tempo, até que encontrei novos problemas. ContentEditables são uma dor e não são realmente fáceis de lidar com React ...

Este JSFiddle demonstra o problema.

Como você pode ver, ao digitar alguns caracteres e clicar em Clear, o conteúdo não é apagado. Isso ocorre porque tentamos redefinir o contenteditable para o último valor dom virtual conhecido.

Então, parece que:

  • Você precisa shouldComponentUpdateevitar saltos na posição do cursor
  • Você não pode confiar no algoritmo de comparação VDOM do React se usar shouldComponentUpdatedesta forma.

Portanto, você precisa de uma linha extra para que, sempre que shouldComponentUpdateretornar sim, você tenha certeza de que o conteúdo DOM está realmente atualizado.

Portanto, a versão aqui adiciona um componentDidUpdatee se torna:

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

O Dom virtual permanece desatualizado e pode não ser o código mais eficiente, mas pelo menos funciona :) Meu bug foi resolvido


Detalhes:

1) Se você colocar shouldComponentUpdate para evitar saltos circunflexos, o contenteditable nunca será processado novamente (pelo menos nas teclas)

2) Se o componente nunca for renderizado novamente no pressionamento de tecla, o React manterá um dom virtual desatualizado para este conteúdo editável.

3) Se o React mantiver uma versão desatualizada do conteúdo editável em sua árvore de dom virtual, se você tentar redefinir o conteúdo editável para o valor desatualizado no dom virtual, durante a comparação do dom virtual, o React calculará que não há alterações em aplique ao DOM!

Isso acontece principalmente quando:

  • você tem um conteúdo editável vazio inicialmente (shouldComponentUpdate = true, prop = "", vdom anterior = N / A),
  • o usuário digita algum texto e você evita renderizações (shouldComponentUpdate = false, prop = text, vdom anterior = "")
  • depois que o usuário clica em um botão de validação, você deseja esvaziar esse campo (shouldComponentUpdate = false, prop = "", vdom anterior = "")
  • como os novos e antigos vdom são "", React não toca no dom.
Sebastien Lorber
fonte
1
Implementei a versão keyPress que alerta o texto quando a tecla Enter é pressionada. jsfiddle.net/kb3gN/11378
Luca Colonnello
@LucaColonnello é melhor você usar {...this.props}para que o cliente possa personalizar esse comportamento de fora
Sebastien Lorber
Oh sim, isso é melhor! Honestamente, tentei essa solução apenas para verificar se o evento keyPress está funcionando em div! Obrigado por esclarecimentos
Luca Colonnello
1
Você poderia explicar como o shouldComponentUpdatecódigo evita saltos circulares?
kmoe
1
@kmoe porque o componente nunca é atualizado se o contentEditable já tiver o texto apropriado (ou seja, ao pressionar a tecla). Atualizar o contentEditable com React faz o cursor pular. Experimente sem conteúdo Editável e veja você mesmo;)
Sebastien Lorber
28

Esta é a solução mais simples que funcionou para mim.

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Abhishek Kanthed
fonte
3
Não há necessidade de downvote isso, funciona! Apenas lembre-se de usar onInputconforme indicado no exemplo.
Sebastian Thomas
Bom e limpo, espero que funcione em muitos dispositivos e navegadores!
JulienRioux
8
Ele move o cursor para o início do texto constantemente quando eu atualizo o texto com o estado React.
Juntae
18

Esta provavelmente não é exatamente a resposta que você está procurando, mas tendo lutado com isso sozinho e tendo problemas com as respostas sugeridas, decidi torná-la descontrolada.

Quando editableprop é false, eu uso textprop como está, mas quando é true, eu mudo para o modo de edição no qual textnão tem efeito (mas pelo menos o navegador não enlouquece). Durante este tempo onChangesão disparados pelo controle. Finalmente, quando eu mudareditable volta para false, ele preenche o HTML com o que foi passado em 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
fonte
Você poderia expandir os problemas que estava tendo com as outras respostas?
NVI,
1
@NVI: Eu preciso de segurança contra injeção, então colocar o HTML como está não é uma opção. Se eu não colocar HTML e usar textContent, recebo todos os tipos de inconsistências do navegador e não consigo implementar shouldComponentUpdatetão facilmente, então nem mesmo isso me salva mais de saltos circulares. Por fim, tenho :empty:beforeplaceholders de pseudoelemento CSS, mas essa shouldComponentUpdateimplementação impediu que o FF e o Safari limpassem o campo quando ele fosse limpo pelo usuário. Levei 5 horas para perceber que posso contornar todos esses problemas com CE não controlado.
Dan Abramov,
Não entendo muito bem como funciona. Você nunca muda editableno UncontrolledContentEditable. Você poderia fornecer um exemplo executável?
NVI,
@NVI: É um pouco difícil, já que uso um módulo interno React aqui. Basicamente, eu configuro editablede fora. Pense em um campo que pode ser editado em linha quando o usuário pressiona “Editar” e deve ser novamente somente leitura quando o usuário pressiona “Salvar” ou “Cancelar”. Então, quando é somente leitura, eu uso adereços, mas paro de olhar para eles sempre que entro no “modo de edição” e só olho para os adereços novamente quando saio dele.
Dan Abramov,
3
Para quem você vai usar este código, React foi renomeado escapeTextForBrowserpara escapeTextContentForBrowser.
wuct
8

Uma vez que quando a edição é concluída, o foco do elemento é sempre perdido, você pode simplesmente usar o gancho onBlur.

<div onBlur={(e)=>{console.log(e.currentTarget.textContent)}} contentEditable suppressContentEditableWarning={true}>
     <p>Lorem ipsum dolor.</p>
</div>
são Lourenço
fonte
5

Eu sugiro usar um mutationObserver para fazer isso. Isso lhe dá muito mais controle sobre o que está acontecendo. Também fornece mais detalhes sobre como o navegador interpreta todas as teclas digitadas

Aqui no 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
fonte
2

Aqui está um componente que incorpora muito disso por lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Ele corrige o evento no 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;
}

Estou usando uma abordagem semelhante com sucesso

Jeff
fonte
1
O autor creditou minha resposta SO em package.json. Este é quase o mesmo código que postei e confirmo que esse código funciona para mim. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber