React.js: onChange เหตุการณ์สำหรับ contentEditable


120

ฉันจะฟังเหตุการณ์การเปลี่ยนแปลงสำหรับ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/


11
หลังจากต่อสู้กับสิ่งนี้ด้วยตัวเองและมีปัญหากับคำตอบที่แนะนำฉันจึงตัดสินใจที่จะทำให้มันไม่มีการควบคุมแทน นั่นคือผมใส่initialValueเข้าไปstateและใช้ในrenderแต่ฉันจะไม่ปล่อยให้ตอบสนองการปรับปรุงมันต่อไป
Dan Abramov

JSFiddle ของคุณไม่ทำงาน
กรีน

ฉันหลีกเลี่ยงการดิ้นรนcontentEditableโดยเปลี่ยนแนวทางของฉัน - แทนที่จะใช้spanหรือparagraphฉันใช้inputพร้อมกับreadonlyคุณลักษณะของมัน
ovidiu-miu

คำตอบ:


79

แก้ไข:ดูคำตอบของ Sebastien Lorberซึ่งแก้ไขข้อบกพร่องในการใช้งานของฉัน


ใช้เหตุการณ์ onInput และทางเลือก onBlur เป็นทางเลือก คุณอาจต้องการบันทึกเนื้อหาก่อนหน้านี้เพื่อป้องกันการส่งเหตุการณ์เพิ่มเติม

ฉันมีสิ่งนี้เป็นการส่วนตัวเป็นฟังก์ชันการเรนเดอร์ของฉัน

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

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

jsbin

ซึ่งใช้ Wrapper อย่างง่ายรอบ ๆ 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 เป็นเมธอด shouldComponentUpdate มันจะกระโดดก็ต่อเมื่อ html prop ไม่ตรงกับ html จริงในองค์ประกอบ เช่นถ้าคุณทำthis.setState({html: "something not in the editable div"}})
Brigand

1
ดี แต่ผมคิดว่าการเรียกร้องให้this.getDOMNode().innerHTMLในshouldComponentUpdateไม่เหมาะสมมากขวา
เซบาสเตียน Lorber

@SebastienLorber ไม่มากที่ดีที่สุด แต่ผมค่อนข้างมั่นใจว่ามันจะดีกว่าที่จะอ่าน HTML กว่าที่จะตั้ง ตัวเลือกเดียวที่ฉันคิดได้คือฟังเหตุการณ์ทั้งหมดที่สามารถเปลี่ยน html และเมื่อสิ่งเหล่านั้นเกิดขึ้นคุณจะแคช html นั่นอาจเร็วกว่าเกือบตลอดเวลา แต่เพิ่มความซับซ้อนมาก นี่เป็นวิธีที่ง่ายและแน่นอน
Brigand

3
นี่เป็นข้อบกพร่องเล็กน้อยจริง ๆ เมื่อคุณต้องการตั้งค่าเป็นค่าstate.html"ทราบ" ล่าสุด React จะไม่อัปเดต DOM เนื่องจาก html ใหม่จะเหมือนกันทุกประการกับที่ React เกี่ยวข้อง (แม้ว่า DOM จริงจะแตกต่างกันก็ตาม) ดูjsfiddle ฉันไม่พบวิธีแก้ปัญหาที่ดีสำหรับเรื่องนี้ดังนั้นยินดีต้อนรับความคิดใด ๆ
univerio

1
@dchest shouldComponentUpdateควรบริสุทธิ์ (ไม่มีผลข้างเคียง).
Brigand

66

แก้ไข 2015

มีคนทำโครงการเกี่ยวกับ NPM ด้วยวิธีแก้ปัญหาของฉัน: https://github.com/lovasoa/react-contenteditable

แก้ไข 06/2016:ฉันเพิ่งค้นพบปัญหาใหม่ที่เกิดขึ้นเมื่อเบราว์เซอร์พยายาม "ฟอร์แมต" html ที่คุณเพิ่งให้ไปซึ่งนำไปสู่การแสดงผลองค์ประกอบเสมอ ดู

แก้ไข 07/2016:นี่คือเนื้อหาการผลิตของฉันการใช้งานที่แก้ไขได้ มีตัวเลือกเพิ่มเติมบางอย่างreact-contenteditableที่คุณอาจต้องการ ได้แก่ :

  • ล็อค
  • API ที่จำเป็นซึ่งอนุญาตให้ฝังส่วนย่อย html
  • ความสามารถในการฟอร์แมตเนื้อหา

สรุป:

วิธีแก้ปัญหาของ FakeRainBrigand ทำงานได้ดีสำหรับฉันมาระยะหนึ่งจนกระทั่งฉันได้รับปัญหาใหม่ ContentEditables เป็นความเจ็บปวดและไม่ใช่เรื่องง่ายที่จะจัดการกับ React ...

JSFiddleนี้แสดงให้เห็นถึงปัญหา

ดังที่คุณเห็นเมื่อคุณพิมพ์อักขระบางตัวและคลิกClearเนื้อหาจะไม่ถูกล้าง นี่เป็นเพราะเราพยายามรีเซ็ต contenteditable เป็นค่า virtual dom ที่ทราบล่าสุด

ดูเหมือนว่า:

  • คุณต้องshouldComponentUpdateป้องกันการกระโดดตำแหน่งคาเร็ต
  • คุณไม่สามารถพึ่งพาอัลกอริทึมที่แตกต่างของ VDOM ของ React ได้หากคุณใช้shouldComponentUpdateวิธีนี้

ดังนั้นคุณต้องมีบรรทัดพิเศษเพื่อที่ว่าเมื่อใดก็ตามที่shouldComponentUpdateส่งกลับใช่คุณจะแน่ใจว่าเนื้อหา DOM ได้รับการอัปเดตจริง

ดังนั้นเวอร์ชันที่นี่จึงเพิ่มcomponentDidUpdateและกลายเป็น:

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

Virtual Dom ยังคงล้าสมัยและอาจไม่ใช่รหัสที่มีประสิทธิภาพสูงสุด แต่อย่างน้อยก็ใช้งานได้ :) ข้อบกพร่องของฉันได้รับการแก้ไขแล้ว


รายละเอียด:

1) หากคุณใส่ shouldComponentUpdate เพื่อหลีกเลี่ยงการกระโดดของคาเร็ตเนื้อหาที่แก้ไขได้จะไม่ส่งกลับ (อย่างน้อยเมื่อกดแป้นพิมพ์)

2) หากคอมโพเนนต์ไม่แสดงผลในจังหวะสำคัญ React จะเก็บโดมเสมือนที่ล้าสมัยไว้สำหรับเนื้อหานี้ที่แก้ไขได้

3) หาก React เก็บเวอร์ชันที่ล้าสมัยของ contenteditable ไว้ใน virtual dom tree แล้วหากคุณพยายามรีเซ็ต contenteditable ให้เป็นค่าที่ล้าสมัยใน virtual dom แล้วในระหว่าง virtual dom ที่แตกต่างกัน React จะคำนวณว่าไม่มีการเปลี่ยนแปลงใด ๆ นำไปใช้กับ DOM!

สิ่งนี้เกิดขึ้นส่วนใหญ่เมื่อ:

  • คุณมี contenteditable ที่ว่างเปล่าในตอนแรก (shouldComponentUpdate = true, prop = "", previous vdom = N / A),
  • ผู้ใช้พิมพ์ข้อความบางส่วนและคุณป้องกันการแสดงผล (shouldComponentUpdate = false, prop = text, previous vdom = "")
  • หลังจากที่ผู้ใช้คลิกปุ่มตรวจสอบความถูกต้องคุณต้องการเว้นช่องนั้น (shouldComponentUpdate = false, prop = "", previous vdom = "")
  • เนื่องจากทั้ง vdom ที่ผลิตใหม่และรุ่นเก่าคือ "" จึงไม่สัมผัสกับโดม

1
ฉันใช้เวอร์ชัน keyPress ที่แจ้งเตือนข้อความเมื่อกดปุ่ม Enter jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello คุณควรใช้{...this.props}เพื่อให้ลูกค้าสามารถปรับแต่งพฤติกรรมนี้จากภายนอกได้
Sebastien Lorber

เออนี่ดีกว่า! สุจริตฉันได้ลองวิธีแก้ปัญหานี้เพื่อตรวจสอบว่าเหตุการณ์ keyPress ทำงานบน div! ขอบคุณสำหรับคำชี้แจง
Luca Colonnello

1
คุณช่วยอธิบายได้ว่าshouldComponentUpdateรหัสป้องกันการกระโดดของคาเร็ตได้อย่างไร
kmoe

1
@kmoe เนื่องจากคอมโพเนนต์จะไม่อัปเดตหาก contentEditable มีข้อความที่เหมาะสมอยู่แล้ว (เช่นเมื่อกดแป้นพิมพ์) การอัปเดตเนื้อหาแก้ไขได้ด้วย React ทำให้คาเร็ตกระโดด ลองโดยไม่มีเนื้อหาแก้ไขได้แล้วดูตัวเอง;)
Sebastien Lorber

28

นี่เป็นวิธีแก้ปัญหาที่ง่ายที่สุดสำหรับฉัน

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

3
ไม่จำเป็นต้องโหวตลงคะแนนนี้ได้ผล! อย่าลืมใช้onInputตามที่ระบุไว้ในตัวอย่าง
Sebastian Thomas

ดีและสะอาดฉันหวังว่ามันจะใช้งานได้กับอุปกรณ์และเบราว์เซอร์จำนวนมาก!
JulienRioux

8
มันย้ายคาเร็ตไปที่จุดเริ่มต้นของข้อความตลอดเวลาเมื่อฉันอัปเดตข้อความด้วยสถานะการตอบสนอง
จุนแท

18

นี่อาจไม่ใช่คำตอบที่คุณกำลังมองหา แต่เมื่อต้องดิ้นรนกับตัวเองและมีปัญหากับคำตอบที่แนะนำฉันจึงตัดสินใจที่จะทำให้มันไม่มีการควบคุมแทน

เมื่อeditableprop เป็นfalseฉันใช้textprop ตามที่เป็นอยู่ แต่เมื่อเป็นtrueเช่นนั้นฉันเปลี่ยนไปใช้โหมดการแก้ไขซึ่งtextไม่มีผลใด ๆ (แต่อย่างน้อยเบราว์เซอร์ก็ไม่แปลกใจ) ในช่วงเวลานี้onChangeจะยิงโดยการควบคุม สุดท้ายเมื่อฉันเปลี่ยนeditableกลับไปfalseมันจะเติม HTML ด้วยสิ่งที่ส่งผ่าน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;

คุณช่วยขยายประเด็นที่คุณพบพร้อมกับคำตอบอื่น ๆ ได้ไหม
NVI

1
@NVI: ฉันต้องการความปลอดภัยจากการฉีดดังนั้นการใส่ HTML เป็นไม่ใช่ทางเลือก ถ้าฉันไม่ใส่ HTML และใช้ textContent ฉันจะได้รับความไม่สอดคล้องกันของเบราว์เซอร์ทุกประเภทและไม่สามารถใช้งานshouldComponentUpdateได้อย่างง่ายดายดังนั้นแม้ว่ามันจะไม่ช่วยฉันจากการกระโดดของคาเร็ตอีกต่อไป สุดท้ายฉันมี:empty:beforeตัวยึดตำแหน่งองค์ประกอบหลอก CSS แต่shouldComponentUpdateการใช้งานนี้ทำให้ FF และ Safari ไม่สามารถล้างฟิลด์ได้เมื่อผู้ใช้ล้าง ฉันใช้เวลา 5 ชั่วโมงกว่าจะรู้ว่าฉันสามารถหลีกเลี่ยงปัญหาเหล่านี้ได้ด้วย CE ที่ไม่มีการควบคุม
Dan Abramov

ฉันไม่ค่อยเข้าใจวิธีการทำงาน คุณไม่เคยเปลี่ยนในeditable UncontrolledContentEditableคุณสามารถให้ตัวอย่างที่รันได้หรือไม่?
NVI

@NVI: มันค่อนข้างยากเนื่องจากฉันใช้ React internal module ที่นี่ .. โดยทั่วไปฉันตั้งค่าeditableจากภายนอก คิดว่าช่องที่แก้ไขได้แบบอินไลน์เมื่อผู้ใช้กด“ แก้ไข” และควรอ่านอีกครั้งเมื่อผู้ใช้กด“ บันทึก” หรือ“ ยกเลิก” ดังนั้นเมื่อเป็นแบบอ่านอย่างเดียวฉันจึงใช้อุปกรณ์ประกอบฉาก แต่ฉันจะหยุดดูทุกครั้งที่เข้าสู่ "โหมดแก้ไข" และมองไปที่อุปกรณ์ประกอบฉากอีกครั้งเมื่อฉันออกจากอุปกรณ์เท่านั้น
Dan Abramov

3
สำหรับคนที่คุณจะใช้รหัสนี้ตอบสนองได้เปลี่ยนชื่อไปescapeTextForBrowser escapeTextContentForBrowser
wuct

8

เนื่องจากเมื่อการแก้ไขเสร็จสิ้นโฟกัสจากองค์ประกอบจะหายไปเสมอคุณสามารถใช้ onBlur hook ได้

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

5

ฉันขอแนะนำให้ใช้ mutationObserver เพื่อทำสิ่งนี้ ช่วยให้คุณควบคุมสิ่งที่เกิดขึ้นได้มากขึ้น นอกจากนี้ยังให้รายละเอียดเพิ่มเติมเกี่ยวกับวิธีที่การเรียกดูตีความการกดแป้นพิมพ์ทั้งหมด

ที่นี่ใน 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

นี่คือส่วนประกอบที่รวมเอาสิ่งนี้โดย lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/index.js

เขาส่องเหตุการณ์ใน 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;
}

ฉันใช้วิธีการเดียวกันนี้ประสบความสำเร็จ


1
ผู้เขียนให้เครดิตคำตอบ SO ของฉันใน package.json นี่เกือบจะเป็นรหัสเดียวกับที่ฉันโพสต์และฉันยืนยันว่ารหัสนี้ใช้ได้กับฉัน github.com/lovasoa/react-contenteditable/blob/master/…
Sebastien Lorber
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.