React.js: acara onChange untuk contentEditable

120

Bagaimana cara mendengarkan perubahan acara untuk contentEditablekontrol berbasis?

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
sumber
11
Setelah berjuang dengan ini sendiri, dan memiliki masalah dengan jawaban yang disarankan, saya memutuskan untuk membuatnya tidak terkontrol. Artinya, saya menempatkan initialValueke dalam statedan menggunakannya dalam render, tapi aku tidak membiarkan Bereaksi pembaruan lebih lanjut.
Dan Abramov
JSFiddle Anda tidak berfungsi
Hijau
Saya menghindari kesulitan dengan contentEditablemengubah pendekatan saya - alih-alih spanatau paragraph, saya telah menggunakan inputbersama dengan readonlyatributnya.
ovidiu-miu

Jawaban:

79

Edit: Lihat jawaban Sebastien Lorber yang memperbaiki bug dalam implementasi saya.


Gunakan peristiwa onInput, dan secara opsional onBlur sebagai fallback. Anda mungkin ingin menyimpan konten sebelumnya untuk mencegah pengiriman acara tambahan.

Saya pribadi memiliki ini sebagai fungsi render saya.

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

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

jsbin

Yang menggunakan pembungkus sederhana ini di sekitar 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;
    }
});
Perampok
sumber
1
@NVI, ini adalah metode shouldComponentUpdate. Ini hanya akan melompat jika prop html tidak sinkron dengan html sebenarnya dalam elemen. misalnya jika Anda melakukannyathis.setState({html: "something not in the editable div"}})
Brigand
1
bagus tapi saya kira panggilan untuk this.getDOMNode().innerHTMLmasuk shouldComponentUpdatetidak terlalu dioptimalkan, benar
Sebastien Lorber
@SebastienLorber tidak terlalu dioptimalkan, tapi saya cukup yakin lebih baik membaca html, daripada menyetelnya. Satu-satunya pilihan lain yang dapat saya pikirkan adalah mendengarkan semua peristiwa yang dapat mengubah html, dan ketika itu terjadi, Anda menyimpan html dalam cache. Itu mungkin akan lebih cepat di sebagian besar waktu, tetapi menambahkan banyak kerumitan. Ini adalah solusi yang sangat pasti dan sederhana.
Brigand
3
Ini sebenarnya sedikit cacat ketika Anda ingin menyetel state.htmlke nilai "yang diketahui" terakhir, React tidak akan memperbarui DOM karena html baru persis sama sejauh menyangkut React (meskipun DOM sebenarnya berbeda). Lihat jsfiddle . Saya belum menemukan solusi yang baik untuk ini, jadi ide apa pun diterima.
univerio
1
@dest shouldComponentUpdateharus murni (tidak memiliki efek samping).
Brigand
66

Edit 2015

Seseorang telah membuat proyek di NPM dengan solusi saya: https://github.com/lovasoa/react-contenteditable

Sunting 06/2016: Saya baru saja mengenalkan masalah baru yang terjadi saat browser mencoba untuk "memformat ulang" html yang baru saja Anda berikan padanya, yang menyebabkan komponen selalu dirender ulang. Lihat

Sunting 07/2016: inilah implementasi contentEditable produksi saya. Ini memiliki beberapa opsi tambahan react-contenteditableyang mungkin Anda inginkan, termasuk:

  • penguncian
  • API imperatif memungkinkan untuk menyematkan fragmen html
  • kemampuan untuk memformat ulang konten

Ringkasan:

Solusi FakeRainBrigand telah bekerja dengan baik untuk saya selama beberapa waktu sampai saya mendapat masalah baru. ContentEditables memang menyebalkan, dan tidak mudah ditangani dengan React ...

JSFiddle ini menunjukkan masalahnya.

Seperti yang Anda lihat, saat Anda mengetik beberapa karakter dan mengkliknya Clear, konten tidak dihapus. Ini karena kami mencoba menyetel ulang konten yang dapat diedit ke nilai dom virtual terakhir yang diketahui.

Jadi sepertinya:

  • Anda perlu shouldComponentUpdatemencegah posisi sisipan melompat
  • Anda tidak dapat mengandalkan algoritma perbedaan VDOM React jika Anda menggunakan shouldComponentUpdatecara ini.

Jadi, Anda memerlukan baris tambahan sehingga setiap kali shouldComponentUpdatemengembalikan ya, Anda yakin konten DOM benar-benar diperbarui.

Jadi versi di sini menambahkan componentDidUpdatedan menjadi:

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

Dom virtual tetap ketinggalan zaman, dan ini mungkin bukan kode yang paling efisien, tetapi setidaknya berfungsi :) Bug saya teratasi


Rincian:

1) Jika Anda meletakkan shouldComponentUpdate untuk menghindari lompatan tanda sisipan, maka konten yang dapat diedit tidak pernah dirender (setidaknya pada penekanan tombol)

2) Jika komponen tidak pernah dirender pada penekanan tombol, maka React menyimpan dom virtual yang sudah ketinggalan zaman agar konten ini dapat diedit.

3) Jika React menyimpan versi lama dari konten yang dapat diedit di pohon dom virtualnya, maka jika Anda mencoba mengatur ulang konten yang dapat diedit ke nilai yang sudah ketinggalan zaman di dom virtual, maka selama perbedaan dom virtual, React akan menghitung bahwa tidak ada perubahan pada terapkan ke DOM!

Ini kebanyakan terjadi ketika:

  • Anda memiliki konten kosong yang dapat diedit pada awalnya (shouldComponentUpdate = true, prop = "", vdom = N / A sebelumnya),
  • pengguna mengetik beberapa teks dan Anda mencegah rendering (shouldComponentUpdate = false, prop = teks, vdom = "" sebelumnya)
  • setelah pengguna mengklik tombol validasi, Anda ingin mengosongkan bidang itu (shouldComponentUpdate = false, prop = "", vdom sebelumnya = "")
  • karena baik vdom yang baru diproduksi maupun yang lama adalah "", React tidak menyentuh dom.
Sebastien Lorber
sumber
1
Saya telah menerapkan versi keyPress yang memberi tahu teks saat tombol enter ditekan. jsfiddle.net/kb3gN/11378
Luca Colonnello
@LucaColonnello sebaiknya Anda gunakan {...this.props}sehingga klien dapat menyesuaikan perilaku ini dari luar
Sebastien Lorber
Oh ya, ini lebih baik! Jujur saya telah mencoba solusi ini hanya untuk memeriksa apakah acara keyPress berfungsi pada div! Terima kasih atas klarifikasi
Luca Colonnello
1
Bisakah Anda menjelaskan bagaimana shouldComponentUpdatekode mencegah lompatan tanda sisipan?
kmoe
1
@kmoe karena komponen tidak pernah memperbarui jika contentEditable sudah memiliki teks yang sesuai (mis. pada penekanan tombol). Memperbarui contentEditable dengan React membuat tanda sisipan melonjak. Cobalah tanpa contentEditable dan lihat diri Anda;)
Sebastien Lorber
28

Ini adalah solusi paling sederhana yang berhasil untuk saya.

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Abhishek Kanthed
sumber
3
Tidak perlu meremehkan ini, itu berhasil! Ingatlah untuk menggunakan onInputseperti yang dinyatakan dalam contoh.
Sebastian Thomas
Bagus dan bersih, saya harap ini berfungsi di banyak perangkat dan browser!
JulienRioux
7
Ini memindahkan tanda sisipan ke awal teks secara konstan saat saya memperbarui teks dengan status React.
Juntae
18

Ini mungkin bukan jawaban yang Anda cari, tetapi setelah berjuang dengan ini sendiri dan memiliki masalah dengan jawaban yang disarankan, saya memutuskan untuk membuatnya tidak terkendali.

Kapan editable prop false, saya menggunakan textprop apa adanya, tetapi ketika itu true, saya beralih ke mode pengeditan yang texttidak berpengaruh (tapi setidaknya browser tidak panik). Selama ini onChangeditembakkan oleh kontrol. Akhirnya, ketika saya mengubahnya editablekembali ke false, itu mengisi HTML dengan apa pun yang diteruskan 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
sumber
Bisakah Anda memperluas masalah yang Anda alami dengan jawaban lain?
NVI
1
@NVI: Saya membutuhkan keamanan dari injeksi, jadi menempatkan HTML apa adanya bukanlah pilihan. Jika saya tidak meletakkan HTML dan menggunakan textContent, saya mendapatkan semua jenis inkonsistensi browser dan tidak dapat menerapkan shouldComponentUpdatedengan mudah sehingga bahkan tidak menyelamatkan saya dari lompatan tanda sisipan lagi. Akhirnya, saya memiliki :empty:beforeplaceholder pseudo-elemen CSS tetapi shouldComponentUpdateimplementasi ini mencegah FF dan Safari membersihkan bidang ketika dihapus oleh pengguna. Butuh waktu 5 jam untuk menyadari bahwa saya dapat menghindari semua masalah ini dengan CE yang tidak terkontrol.
Dan Abramov
Saya tidak begitu mengerti cara kerjanya. Anda tidak pernah berubah editabledi UncontrolledContentEditable. Bisakah Anda memberikan contoh yang dapat dijalankan?
NVI
@ NVI: Agak susah karena saya menggunakan modul internal React disini .. Pada dasarnya saya set editabledari luar. Pikirkan bidang yang dapat diedit secara inline ketika pengguna menekan "Edit" dan harus dibaca lagi ketika pengguna menekan "Simpan" atau "Batal". Jadi ketika itu hanya bisa dibaca, saya menggunakan alat peraga, tetapi saya berhenti melihatnya setiap kali saya masuk ke "mode edit" dan hanya melihat alat peraga lagi ketika saya keluar.
Dan Abramov
3
Untuk siapa Anda akan menggunakan kode ini, React telah berganti nama escapeTextForBrowsermenjadi escapeTextContentForBrowser.
wuct
8

Karena ketika pengeditan selesai, fokus dari elemen selalu hilang, Anda cukup menggunakan hook onBlur.

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

Saya sarankan menggunakan mutationObserver untuk melakukan ini. Ini memberi Anda lebih banyak kendali atas apa yang sedang terjadi. Ini juga memberi Anda detail lebih lanjut tentang cara penelusuran menafsirkan semua penekanan tombol

Di sini, di 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
sumber
2

Berikut adalah komponen yang menggabungkan banyak hal ini dengan lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

Dia menggeser acara di 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;
}

Saya berhasil menggunakan pendekatan serupa

Jeff
sumber
1
Penulis telah memberikan jawaban SO saya di package.json. Ini hampir sama dengan kode yang saya posting dan saya mengonfirmasi bahwa kode ini berfungsi untuk saya. github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber