React.js: événement onChange pour contentEditable


120

Comment écouter un événement de changement pour un contentEditablecontrôle basé sur?

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
Ayant moi-même lutté avec cela et ayant des problèmes avec les réponses suggérées, j'ai décidé de le rendre incontrôlé à la place. C'est, je mets initialValueen stateet l' utiliser dans render, mais je ne laisse pas React jour davantage.
Dan Abramov

Votre JSFiddle ne fonctionne pas
Vert

J'ai évité de lutter avec contentEditableen changeant mon approche - au lieu d'un spanou paragraph, j'ai utilisé un inputavec son readonlyattribut.
ovidiu-miu

Réponses:


79

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


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

J'aurais personnellement ceci 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;
    }
});

1
@NVI, c'est la méthode shouldComponentUpdate. Cela ne sautera que si le prop html n'est pas synchronisé avec le html réel de l'élément. par exemple si vous l'avez faitthis.setState({html: "something not in the editable div"}})
Brigand

1
sympa mais je suppose que l'appel à this.getDOMNode().innerHTMLen shouldComponentUpdateest pas très optimisé droit
Sébastien Lorber

@SebastienLorber n'est pas très optimisé, mais je suis presque sûr qu'il vaut mieux lire le html que de le définir. La seule autre option à laquelle je puisse penser est d'écouter tous les événements qui pourraient changer le html, et lorsque ceux-ci se produisent, vous mettez le html en cache. Ce serait probablement plus rapide la plupart du temps, mais cela ajouterait beaucoup de complexité. C'est la solution très sûre et simple.
Brigand

3
Ceci est en fait légèrement défectueux lorsque vous souhaitez définir state.htmlla dernière valeur "connue", React ne mettra pas à jour le DOM car le nouveau code HTML est exactement le même en ce qui concerne React (même si le DOM réel est différent). Voir jsfiddle . Je n'ai pas trouvé de bonne solution pour cela, toutes les idées sont donc les bienvenues.
univerio

1
@dchest shouldComponentUpdatedoit être pur (ne pas avoir d'effets secondaires).
Brigand

66

Modifier 2015

Quelqu'un a réalisé 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 html que vous venez de lui donner, conduisant à un re-rendu des composants. Voir

Edit 07/2016: voici mon contenu de production Implémentation modifiable. Il propose des options supplémentaires react-contenteditableque vous pourriez souhaiter, notamment:

  • verrouillage
  • API impérative permettant d'incorporer des fragments html
  • possibilité de reformater le contenu

Résumé:

La solution de FakeRainBrigand a très bien fonctionné pour moi pendant un certain temps jusqu'à ce que j'aie de nouveaux problèmes. ContentEditables est pénible, et n'est pas vraiment facile à gérer avec React ...

Ce JSFiddle illustre le problème.

Comme vous pouvez le voir, lorsque vous tapez des caractères et cliquez sur Clear, le contenu n'est pas effacé. C'est parce que nous essayons de réinitialiser le contenteditable à la dernière valeur de dom virtuelle connue.

Il semble donc que:

  • Vous devez shouldComponentUpdateéviter les sauts de position du curseur
  • Vous ne pouvez pas compter sur l'algorithme de différence VDOM de React si vous utilisez shouldComponentUpdatecette méthode.

Vous avez donc besoin d'une ligne supplémentaire pour que chaque fois que shouldComponentUpdateretourne oui, vous soyez sûr que le contenu DOM est réellement mis à jour.

Donc la version ici ajoute un componentDidUpdateet 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 bogue est résolu


Détails:

1) Si vous mettez shouldComponentUpdate pour éviter les sauts de curseur, alors les contenteditable ne sont jamais renvoyés (au moins sur les frappes)

2) Si le composant ne se rend jamais sur un coup de touche, React conserve un dom virtuel obsolète pour ce contenu modifiable.

3) Si React conserve une version obsolète du contenteditable dans son arborescence de dom virtuel, alors si vous essayez de réinitialiser le contenteditable à la valeur obsolète dans le dom virtuel, alors pendant la différence de dom virtuel, React calculera qu'il n'y a pas de changement dans postulez au DOM!

Cela se produit principalement lorsque:

  • vous avez un contenu vide initialement modifiable (shouldComponentUpdate = true, prop = "", previous vdom = N / A),
  • l'utilisateur tape du texte et vous empêchez les rendus (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • après que l'utilisateur clique sur un bouton de validation, vous souhaitez vider ce champ (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • comme le vdom nouvellement produit et l'ancien sont "", React ne touche pas le dom.

1
J'ai implémenté la version keyPress qui alerte le texte lorsque la touche Entrée est enfoncée. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello vous feriez mieux d'utiliser {...this.props}pour que le client puisse personnaliser ce comportement de l'extérieur
Sebastien Lorber

Oh ouais, c'est mieux! Honnêtement, j'ai essayé cette solution uniquement pour vérifier si l'événement keyPress fonctionnait sur div! Merci pour les clarifications
Luca Colonnello

1
Pouvez-vous expliquer comment le shouldComponentUpdatecode empêche les sauts d'insertion?
kmoe

1
@kmoe car le composant ne se met jamais à jour si contentEditable a déjà le texte approprié (c'est-à-dire sur une frappe). La mise à jour du contenu modifiable avec React fait sauter le curseur. Essayez sans contenuEditable et voyez-vous;)
Sebastien Lorber

28

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

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

3
Pas besoin de voter contre cela, cela fonctionne! N'oubliez pas d'utiliser onInputcomme indiqué dans l'exemple.
Sebastian Thomas

Joli et propre, j'espère que cela fonctionne sur de nombreux appareils et navigateurs!
JulienRioux

8
Il déplace constamment le curseur au début du texte lorsque je mets à jour le texte avec l'état React.
Juntae

18

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

Quand editableprop est false, j'utilise textprop tel quel, mais quand c'est le cas true, je passe en mode d'édition dans lequel textn'a aucun effet (mais au moins le navigateur ne panique pas). Pendant ce temps onChangesont tirés par le contrôle. Enfin, lorsque je editablereviens à false, il remplit le HTML avec tout ce qui a été passé 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;

Pourriez-vous développer les problèmes que vous rencontriez avec les autres réponses?
NVI

1
@NVI: J'ai besoin de sécurité contre l'injection, donc mettre le HTML tel quel n'est pas une option. Si je ne mets pas de HTML et n'utilise pas textContent, j'obtiens toutes sortes d'incohérences dans le navigateur et je ne peux pas l'implémenter shouldComponentUpdatesi facilement, même si cela ne me sauve plus des sauts de curseur. Enfin, j'ai des :empty:beforeespaces réservés de pseudo-éléments CSS mais cette shouldComponentUpdateimplémentation a empêché FF et Safari de nettoyer le champ lorsqu'il est effacé par l'utilisateur. Il m'a fallu 5 heures pour réaliser que je peux éviter tous ces problèmes avec une CE incontrôlée.
Dan Abramov

Je ne comprends pas très bien comment cela fonctionne. Vous changez jamais editabledans UncontrolledContentEditable. Pouvez-vous fournir un exemple exécutable?
NVI

@NVI: C'est un peu dur puisque j'utilise ici un module interne React .. En gros, je me suis installé editablede l'extérieur. Pensez à un champ qui peut être édité en ligne lorsque l'utilisateur appuie sur «Modifier» et devrait être à nouveau en lecture seule lorsque l'utilisateur appuie sur «Enregistrer» ou «Annuler». Donc quand il est en lecture seule, j'utilise des accessoires, mais j'arrête de les regarder chaque fois que j'entre en «mode édition» et je ne regarde à nouveau les accessoires que lorsque je le quitte.
Dan Abramov

3
Pour que vous allez utiliser ce code, React a renommé escapeTextForBrowserpour escapeTextContentForBrowser.
wuct

8

Puisque lorsque la modification est terminée, le focus de l'élément est toujours perdu, vous pouvez simplement utiliser le hook onBlur.

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

5

Je suggère d'utiliser un mutationObserver pour ce 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 la navigation interprète toutes les frappes

Ici dans 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

Voici un composant qui en incorpore une grande partie par lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Il cale 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


1
L'auteur a crédité ma réponse SO dans package.json. C'est presque le même code que j'ai posté et je confirme que ce code fonctionne pour moi. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.