React.js: contentEditable için onChange olayı


120

contentEditable-Based kontrol için değişim olayını nasıl dinlerim ?

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
Bununla kendim mücadele ettikten ve önerilen cevaplarla ilgili sorunlar yaşadığım için, kontrolsüz yapmaya karar verdim. Yani, initialValueiçine koyuyorum stateve kullanıyorum render, ancak React'in daha fazla güncellemesine izin vermiyorum.
Dan Abramov

JSFiddle çalışmıyor
Green

contentEditableYaklaşımımı değiştirerek mücadele etmekten kaçındım - a spanveya yerine , niteliğiyle birlikte paragraphbir kullandım . inputreadonly
ovidiu-miu

Yanıtlar:


79

Düzenleme: Sebastien Lorber'ın uygulamamdaki bir hatayı düzelten cevabına bakın .


OnInput olayını ve isteğe bağlı olarak onBlur'u yedek olarak kullanın. Fazladan olayların gönderilmesini önlemek için önceki içerikleri kaydetmek isteyebilirsiniz.

Bunu kişisel olarak render işlevim olarak alırdım.

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

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

jsbin

Bu, contentEditable etrafında bu basit sarmalayıcıyı kullanır.

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, shouldComponentUpdate yöntemidir. Yalnızca html prop, öğedeki gerçek html ile senkronize değilse atlar. örneğin, yaptıysanthis.setState({html: "something not in the editable div"}})
Brigand

1
güzel ama aramayı tahmin this.getDOMNode().innerHTMLde shouldComponentUpdateçok doğru optimize edilmediği
Sebastien Lorber

@SebastienLorber çok optimize edilmemiş, ancak html'yi okumanın onu ayarlamaktan daha iyi olduğuna eminim. Aklıma gelen diğer tek seçenek html'yi değiştirebilecek tüm olayları dinlemektir ve bunlar gerçekleştiğinde html'yi önbelleğe alırsınız. Bu muhtemelen çoğu zaman daha hızlı olacaktır, ancak çok fazla karmaşıklık ekler. Bu çok kesin ve basit bir çözümdür.
Brigand

3
Bu aslında state.htmlson "bilinen" değere ayarlamak istediğinizde biraz kusurludur , React DOM'u güncellemeyecektir çünkü yeni html, React söz konusu olduğunda tamamen aynıdır (gerçek DOM farklı olsa bile). Jsfiddle'a bakın . Bunun için iyi bir çözüm bulamadım, bu yüzden herhangi bir fikre açığım.
univerio

1
@dchest shouldComponentUpdatesaf olmalıdır (yan etkileri olmamalıdır).
Brigand

66

2015'i Düzenle

Birisi benim çözümümle NPM üzerine bir proje yaptı: https://github.com/lovasoa/react-contenteditable

Düzenleme 06/2016: Tarayıcı az önce verdiğiniz html'yi "yeniden biçimlendirmeye" çalıştığında ortaya çıkan yeni bir sorunu kodladım ve bu da bileşenin her zaman yeniden oluşturulmasına neden oluyor. Görmek

Düzenleme 07/2016: İşte benim üretim içeriğimDüzenlenebilir uygulama. Aşağıdakiler react-contenteditabledahil olmak üzere isteyebileceğiniz bazı ek seçeneklere sahiptir :

  • kilitleme
  • html parçalarının yerleştirilmesine izin veren zorunlu API
  • içeriği yeniden biçimlendirme yeteneği

Özet:

FakeRainBrigand'ın çözümü, yeni sorunlar alana kadar bir süredir benim için oldukça iyi çalıştı. ContentEditables bir acıdır ve React ile başa çıkmak gerçekten kolay değildir ...

Bu JSFiddle , sorunu göstermektedir.

Gördüğünüz gibi, bazı karakterler yazıp üzerine tıkladığınızda Clear, içerik temizlenmiyor. Bunun nedeni, içerik düzenlenebilir olanı bilinen son sanal dom değerine sıfırlamaya çalışmamızdır.

Yani öyle görünüyor:

  • shouldComponentUpdateDüzeltme pozisyonu atlamalarını önlemeniz gerekir
  • shouldComponentUpdateBu şekilde kullanırsanız React'in VDOM fark algoritmasına güvenemezsiniz .

Dolayısıyla shouldComponentUpdate, evet döndürdüğünüzde DOM içeriğinin gerçekten güncellendiğinden emin olmanız için fazladan bir satıra ihtiyacınız vardır.

Yani buradaki sürüm a ekler componentDidUpdateve şöyle olur:

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

Sanal alan güncelliğini yitiriyor ve en verimli kod olmayabilir, ancak en azından çalışıyor :) Hatam çözüldü


Detaylar:

1) İmleç atlamalarını önlemek için shouldComponentUpdate'i koyarsanız, memnun olan asla yeniden oluşturulmaz (en azından tuş vuruşlarında)

2) Bileşen hiçbir zaman tuş vuruşunda yeniden oluşturmazsa, React bu içerik için güncel olmayan bir sanal alan tutar.

3) React, içerik düzenlenebilirliğin güncel olmayan bir sürümünü sanal dom ağacında tutarsa, sanal domainde içerik düzenlenebilirliği güncel olmayan değere sıfırlamaya çalışırsanız, sanal dom farkı sırasında React, üzerinde herhangi bir değişiklik olmadığını hesaplayacaktır. DOM'a başvurun!

Bu çoğunlukla şu durumlarda olur:

  • başlangıçta memnun edici boş bir öğeniz var (shouldComponentUpdate = true, prop = "", önceki vdom = N / A),
  • kullanıcı bir miktar metin yazar ve siz işlemeleri engellersiniz (shouldComponentUpdate = false, prop = text, önceki vdom = "")
  • kullanıcı bir doğrulama düğmesini tıkladıktan sonra, bu alanı boşaltmak istersiniz (shouldComponentUpdate = false, prop = "", önceki vdom = "")
  • Hem yeni üretilmiş hem de eski vdom "" olduğundan, React doma dokunmaz.

1
Enter tuşuna basıldığında metni uyaran keyPress sürümü uyguladım. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello {...this.props}, müşterinin bu davranışı dışarıdan özelleştirebilmesi için kullansanız iyi olur
Sebastien Lorber

Oh evet, bu daha iyi! Dürüst olmak gerekirse, bu çözümü yalnızca keyPress olayının div üzerinde çalışıp çalışmadığını kontrol etmek için denedim! Açıklamalar için teşekkürler
Luca Colonnello

1
shouldComponentUpdateKodun düzeltme atlamalarını nasıl önlediğini açıklayabilir misiniz ?
kmoe

1
@kmoe çünkü contentEditable zaten uygun metne sahipse (yani tuş vuruşunda) bileşen hiçbir zaman güncellenmez. ContentEditable'ın React ile güncellenmesi imleci atlar. İçerik olmadan deneyin Düzenlenebilir ve kendinizi görün;)
Sebastien Lorber

28

Bu, benim için çalışan en basit çözümdür.

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

3
Bunu olumsuz oylamaya gerek yok, işe yarıyor! onInputÖrnekte belirtildiği gibi kullanmayı unutmayın .
Sebastian Thomas

Güzel ve temiz, umarım birçok cihazda ve tarayıcıda çalışır!
JulienRioux

8
Metni React durumuyla güncellediğimde imleci sürekli olarak metnin başına taşır.
Juntae

18

Muhtemelen tam olarak aradığınız cevap bu değil, ancak bununla kendim mücadele edip önerilen cevaplarla ilgili sorunlar yaşadığım için kontrolsüz yapmaya karar verdim.

Ne zaman editableprop false, kullandığım textolduğu gibi pervane, ama olduğunda true, ben hangi düzenleme modu geçiş text(ama en azından tarayıcı korkutmuyor) hiçbir etkisi yoktur. Bu süre onChangeiçinde kontrol tarafından ateşlenir. Son olarak, konumuna editablegeri döndüğümde false, HTML'yi içeri aktarılmış olanla doldurur 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;

Diğer cevaplarla yaşadığınız sorunları biraz daha açabilir misiniz?
NVI

1
@NVI: Enjeksiyondan güvenliğe ihtiyacım var, bu yüzden HTML'yi olduğu gibi koymak bir seçenek değil. HTML koymazsam ve textContent kullanmazsam, her türlü tarayıcı tutarsızlığını yaşıyorum ve shouldComponentUpdateo kadar kolay uygulayamıyorum ki, bu bile beni imleç atlamalarından kurtarmıyor. Son olarak, CSS sözde öğe :empty:beforeyer tutuculara sahibim ancak bu shouldComponentUpdateuygulama FF ve Safari'nin kullanıcı tarafından temizlendiğinde alanı temizlemesini engelledi. Kontrolsüz CE ile tüm bu sorunlardan kaçabileceğimi fark etmem 5 saatimi aldı.
Dan Abramov

Nasıl çalıştığını tam olarak anlamıyorum. Sen hiç değişmez editableiçinde UncontrolledContentEditable. Çalıştırılabilir bir örnek verebilir misiniz?
NVI

@NVI: Burada bir React dahili modülü kullandığım için biraz zor .. Temelde editabledışarıdan ayarlarım . Kullanıcı "Düzenle" ye bastığında yerinde düzenlenebilecek ve kullanıcı "Kaydet" veya "İptal" e bastığında yeniden salt okunur olması gereken bir alan düşünün. Yani salt okunur olduğunda, sahne donanımlarını kullanıyorum, ancak "düzenleme moduna" her girdiğimde onlara bakmayı bırakıyorum ve sadece çıktığımda sahne malzemelerine tekrar bakıyorum.
Dan Abramov

3
Bu kodu kullanacağız kime için, adını vardır React escapeTextForBrowseriçin escapeTextContentForBrowser.
18:17

8

Düzenleme tamamlandığında, öğedeki odak her zaman kaybolduğundan, basitçe onBlur kancasını kullanabilirsiniz.

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

5

Bunu yapmak için bir mutationObserver kullanmanızı öneririm. Size neler olduğu üzerinde daha fazla kontrol sağlar. Ayrıca, göz atmanın tüm tuş vuruşlarını nasıl yorumladığına dair daha fazla ayrıntı verir.

Burada TypeScript'te

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

İşte lovasoa tarafından bunun çoğunu içeren bir bileşen: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

EmitChange'de olayı değiştirir

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

Benzer bir yaklaşımı başarıyla kullanıyorum


1
Yazar, SO cevabımı package.json'a koydu. Bu, yayınladığım kodun neredeyse aynısı ve bu kodun benim için çalıştığını onaylıyorum. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.