web-dev-qa-db-fra.com

React.js: événement onChange pour contentEditable

Comment écouter un événement change pour le contrôle basé sur contentEditable?

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/

90
NVI

Edit: Voir réponse de Sébastien Lorber qui corrige un bug dans mon implémentation.


Utilisez l'événement onInput et éventuellement onBlur comme solution de secours. Vous voudrez peut-être sauvegarder le contenu précédent pour empêcher l'envoi d'événements supplémentaires.

J'aurais personnellement cela comme fonction de rendu.

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

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

jsbin

Qui utilise ce simple wrapper autour 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;
    }
});
66
Brigand

Éditer 2015

Quelqu'un a créé un projet sur NPM avec ma solution: https://github.com/lovasoa/react-contenteditable

Edit 06/2016: Je viens de rencontrer un nouveau problème qui se produit lorsque le navigateur essaie de "reformater" le code HTML que vous venez de lui donner, ce qui conduit à composant toujours re-rendu. Voir

Edit 07/2016: voici mon implémentation contentEditable de production. Il a quelques options supplémentaires sur react-contenteditable que vous voudrez peut-être, y compris:

  • verrouillage
  • aPI impérative permettant d'intégrer des fragments html
  • possibilité de reformater le contenu

Sommaire:

La solution de FakeRainBrigand a fonctionné assez bien pour moi pendant un certain temps, jusqu'à ce que je rencontre de nouveaux problèmes. ContentEditables sont une douleur, et ne sont pas vraiment faciles à traiter avec React ...

Ceci JSFiddle illustre le problème.

Comme vous pouvez le constater, lorsque vous tapez des caractères et cliquez sur Clear, le contenu n’est pas effacé. En effet, nous essayons de réinitialiser la propriété contenteditable à la dernière valeur de dom virtuelle connue.

Il semble donc que:

  • Vous avez besoin de shouldComponentUpdate pour empêcher les sauts de position de curseur
  • Vous ne pouvez pas vous fier à l'algorithme de différenciation VDOM de React si vous utilisez shouldComponentUpdate de cette façon.

Vous avez donc besoin d’une ligne supplémentaire pour que chaque fois que shouldComponentUpdate renvoie oui, vous êtes sûr que le contenu du DOM est réellement mis à jour.

Donc la version ici ajoute un componentDidUpdate et devient:

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

Le dom virtuel reste obsolète, et ce n'est peut-être pas le code le plus efficace, mais au moins ça marche :) Mon bug est résol


Détails:

1) Si vous mettez devraitComponentUpdate pour éviter les sauts de curseur, alors le contenteditable ne redirige jamais (au moins sur les frappes au clavier)

2) Si le composant ne réapparaît jamais lors de la frappe d'une touche, alors React conserve un dom virtuel obsolète pour ce contenu modifiable.

3) Si React conserve une version obsolète de la propriété contenteditable dans son arborescence de dom virtuelle, si vous essayez de réinitialiser la propriété contenteditable à la valeur périmée du dom virtuel, alors, pendant le diff diff virtuel, React calculera qu'il n'y a aucune modification à appliquer au DOM!

Cela arrive surtout quand:

  • vous avez initialement un contenteditable vide (shouldComponentUpdate = true, prop = "", précédent vdom = N/A),
  • l'utilisateur tape du texte et vous empêchez les rendus (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • une fois que l'utilisateur a cliqué sur un bouton de validation, vous souhaitez vider ce champ (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • car vdom nouvellement produit et ancien sont "", React ne touche pas le dom.
61
Sebastien Lorber

Ce n'est probablement pas exactement la réponse que vous recherchez, mais après avoir moi-même eu du mal à résoudre ce problème et avoir eu des problèmes avec les réponses suggérées, j'ai décidé de le rendre non contrôlé à la place.

Lorsque editable prop est false, j'utilise text prop tel quel, mais lorsqu'il s'agit de true, je passe en mode d'édition dans lequel text n'a aucun effet (mais au moins le navigateur ne panique pas). Pendant ce temps, onChange sont déclenchés par le contrôle. Enfin, lorsque je remplace editable par false, il remplit le code HTML avec tout ce qui a été passé dans 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;
14
Dan Abramov

Je suggère d'utiliser un mutationObserver pour le faire. Cela vous donne beaucoup plus de contrôle sur ce qui se passe. Il vous donne également plus de détails sur la façon dont le parcours interprète toutes les frappes.

Ici 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;
    }
}
4
klugjo

Voici un composant qui intègre une grande partie de cela par lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Il shims l'événement dans le 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;
}

J'utilise une approche similaire avec succès

2
Jeff

C'est la solution la plus simple qui a fonctionné pour moi.

<div
  contentEditable='true'
  onInput = {e => {console.log('text of div', e.currentTarget.textContent)}}
>
Text in div
</div>
2
Abhishek Kanthed

Comme lorsque la modification est terminée, la focalisation de l'élément est toujours perdue, vous pouvez simplement utiliser le crochet onBlur.

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