React.js: sự kiện onChange cho contentEditable


120

Làm cách nào để lắng nghe sự kiện thay đổi đối với contentEditableđiều khiển dựa trên cơ sở?

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
Bản thân đã đấu tranh với điều này và gặp vấn đề với các câu trả lời được đề xuất, thay vào đó, tôi quyết định biến nó thành không kiểm soát. Đó là, tôi đã đưa initialValuevào statevà sử dụng nó render, nhưng tôi không để React cập nhật thêm.
Dan Abramov

JSFiddle của bạn không hoạt động
Màu xanh lá cây

Tôi tránh gặp khó khăn bằng contentEditablecách thay đổi cách tiếp cận của mình - thay vì spanhoặc paragraph, tôi đã sử dụng một inputcùng với readonlythuộc tính của nó .
ovidiu-miu

Câu trả lời:


79

Chỉnh sửa: Xem câu trả lời của Sebastien Lorber đã sửa một lỗi trong quá trình triển khai của tôi.


Sử dụng sự kiện onInput và tùy chọn onBlur làm dự phòng. Bạn có thể muốn lưu các nội dung trước đó để ngăn việc gửi thêm các sự kiện.

Cá nhân tôi muốn có điều này làm chức năng kết xuất của tôi.

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

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

jsbin

Trong đó sử dụng trình bao bọc đơn giản này xung quanh 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, đó là phương thức shouldComponentUpdate. Nó sẽ chỉ nhảy nếu phần hỗ trợ html không đồng bộ với html thực trong phần tử. ví dụ nếu bạn đã làmthis.setState({html: "something not in the editable div"}})
Brigand

1
đẹp, nhưng tôi đoán cuộc gọi đến this.getDOMNode().innerHTMLtrong shouldComponentUpdatekhông được tối ưu hóa rất đúng
Sebastien Lorber

@SebastienLorber không được tối ưu hóa cho lắm , nhưng tôi khá chắc rằng đọc html tốt hơn là đặt nó. Lựa chọn khác duy nhất mà tôi có thể nghĩ đến là lắng nghe tất cả các sự kiện có thể thay đổi html và khi những sự kiện đó xảy ra, bạn lưu vào bộ nhớ cache của html. Điều đó có lẽ sẽ nhanh hơn hầu hết thời gian, nhưng thêm rất nhiều phức tạp. Đây là giải pháp rất chắc chắn và đơn giản.
Brigand

3
Điều này thực sự hơi thiếu sót khi bạn muốn đặt thành state.htmlgiá trị "đã biết" cuối cùng, React sẽ không cập nhật DOM vì html mới hoàn toàn giống với React (mặc dù DOM thực tế là khác nhau). Xem jsfiddle . Tôi đã không tìm thấy một giải pháp tốt cho việc này, vì vậy mọi ý tưởng đều được hoan nghênh.
univerio

1
@dchest shouldComponentUpdatephải tinh khiết (không có tác dụng phụ).
Brigand

66

Chỉnh sửa 2015

Ai đó đã thực hiện một dự án về NPM với giải pháp của tôi: https://github.com/lovasoa/react-contentitable

Chỉnh sửa 06/2016: Tôi vừa phát hiện ra một vấn đề mới xảy ra khi trình duyệt cố gắng "định dạng lại" html mà bạn vừa đưa cho anh ta, dẫn đến thành phần luôn hiển thị lại. Xem

Chỉnh sửa 07/2016: đây là nội dung sản xuất của tôi Nó có một số tùy chọn bổ sung react-contenteditablemà bạn có thể muốn, bao gồm:

  • khóa
  • API bắt buộc cho phép nhúng các đoạn html
  • khả năng định dạng lại nội dung

Tóm lược:

Giải pháp của FakeRainBrigand đã hoạt động khá tốt đối với tôi trong một thời gian cho đến khi tôi gặp vấn đề mới. ContentEditables là một khó khăn và không thực sự dễ dàng để đối phó với React ...

JSFiddle này giải thích vấn đề.

Như bạn có thể thấy, khi bạn nhập một số ký tự và nhấp vào Clear, nội dung sẽ không bị xóa. Điều này là do chúng tôi cố gắng đặt lại nội dung có thể thay đổi thành giá trị dom ảo cuối cùng đã biết.

Vì vậy, có vẻ như rằng:

  • Bạn cần shouldComponentUpdatengăn chặn nhảy vị trí dấu mũ
  • Bạn không thể dựa vào thuật toán khác biệt VDOM của React nếu bạn sử dụng shouldComponentUpdatecách này.

Vì vậy, bạn cần thêm một dòng để bất cứ khi nào shouldComponentUpdatetrả về có, bạn chắc chắn rằng nội dung DOM đã thực sự được cập nhật.

Vì vậy, phiên bản ở đây thêm một componentDidUpdatevà trở thành:

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 ảo vẫn lỗi thời và nó có thể không phải là mã hiệu quả nhất, nhưng ít nhất nó vẫn hoạt động :) Lỗi của tôi đã được giải quyết


Chi tiết:

1) Nếu bạn đặt shouldComponentUpdate để tránh nhảy dấu mũ, thì nội dung có thể chỉnh sửa sẽ không bao giờ hiển thị (ít nhất là khi nhấn phím)

2) Nếu thành phần không bao giờ hiển thị trên hành trình phím, thì React sẽ giữ một dom ảo đã lỗi thời để có thể nội dung này.

3) Nếu React giữ một phiên bản lỗi thời của contenteditable trong cây dom ảo của nó, thì nếu bạn cố gắng đặt lại contenteditable thành giá trị đã lỗi thời trong dom ảo, thì trong quá trình khác biệt dom ảo, React sẽ tính toán rằng không có thay đổi nào đối với áp dụng cho DOM!

Điều này chủ yếu xảy ra khi:

  • ban đầu bạn có một nội dung trống (nênComponentUpdate = true, prop = "", vdom trước đó = N / A),
  • người dùng nhập một số văn bản và bạn ngăn kết xuất (shouldComponentUpdate = false, prop = text, vdom trước đó = "")
  • sau khi người dùng nhấp vào nút xác thực, bạn muốn trống trường đó (shouldComponentUpdate = false, prop = "", vdom trước đó = "")
  • vì cả vdom mới được sản xuất và cũ đều là "", React không chạm vào dom.

1
Tôi đã triển khai phiên bản keyPress cảnh báo văn bản khi nhấn phím enter. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello bạn nên sử dụng {...this.props}để khách hàng có thể tùy chỉnh hành vi này từ bên ngoài
Sebastien Lorber

Oh yeah, cái này tốt hơn! Thành thật mà nói, tôi đã thử giải pháp này chỉ để kiểm tra xem sự kiện keyPress có hoạt động trên div hay không! Cám ơn làm rõ
Luca Colonello

1
Bạn có thể giải thích cách shouldComponentUpdatemã ngăn chặn nhảy dấu mũ không?
kmoe

1
@kmoe bởi vì thành phần không bao giờ cập nhật nếu contentEditable đã có văn bản thích hợp (tức là trên tổ hợp phím). Cập nhật nội dung Có thể thay đổi với React làm cho dấu mũ nhảy lên. Hãy thử mà không contentEditable và nhìn thấy mình;)
Sebastien Lorber

28

Đây là giải pháp đơn giản nhất phù hợp với tôi.

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

3
Không cần phản đối điều này, nó hoạt động! Chỉ cần nhớ sử dụng onInputnhư đã nêu trong ví dụ.
Sebastian Thomas

Đẹp và sạch, tôi hy vọng nó hoạt động trên nhiều thiết bị và trình duyệt!
JulienRioux

7
Nó di chuyển dấu mũ đến đầu văn bản liên tục khi tôi cập nhật văn bản với trạng thái React.
Juntae

18

Đây có thể không phải là câu trả lời chính xác mà bạn đang tìm kiếm, nhưng bản thân tôi đã đấu tranh với điều này và gặp vấn đề với các câu trả lời được đề xuất, tôi quyết định không kiểm soát nó.

Khi có editableprop false, tôi sử dụng textprop như vậy, nhưng khi có true, tôi chuyển sang chế độ chỉnh sửa mà textkhông có tác dụng (nhưng ít nhất thì trình duyệt không cảm thấy khó chịu). Trong thời gian này onChangeđược bắn bởi kiểm soát. Cuối cùng, khi tôi thay đổi editabletrở lại false, nó sẽ điền vào HTML với bất kỳ thứ gì đã được chuyển vào 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;

Bạn có thể mở rộng các vấn đề bạn đang gặp phải với các câu trả lời khác không?
NVI

1
@NVI: Tôi cần sự an toàn từ việc tiêm, vì vậy đặt HTML như hiện tại không phải là một lựa chọn. Nếu tôi không đặt HTML và sử dụng textContent, tôi sẽ gặp phải tất cả các loại mâu thuẫn trình duyệt và không thể triển khai shouldComponentUpdatedễ dàng như vậy nên ngay cả khi nó cũng không giúp tôi thoát khỏi các bước nhảy dấu mũ nữa. Cuối cùng, tôi có :empty:beforetrình giữ chỗ phần tử giả CSS nhưng việc shouldComponentUpdatetriển khai này đã ngăn FF và Safari dọn dẹp trường khi nó bị người dùng xóa. Tôi đã mất 5 giờ để nhận ra rằng tôi có thể vượt qua tất cả những vấn đề này với CE không kiểm soát.
Dan Abramov

Tôi không hiểu nó hoạt động như thế nào. Bạn không bao giờ thay đổi editabletrong UncontrolledContentEditable. Bạn có thể cung cấp một ví dụ có thể chạy được không?
NVI

@NVI: Hơi khó vì tôi sử dụng mô-đun nội bộ React ở đây .. Về cơ bản, tôi đặt editabletừ bên ngoài. Hãy nghĩ đến một trường có thể được chỉnh sửa nội dòng khi người dùng nhấn “Chỉnh sửa” và chỉ được đọc lại khi người dùng nhấn “Lưu” hoặc “Hủy”. Vì vậy, khi nó chỉ được đọc, tôi sử dụng các đạo cụ, nhưng tôi ngừng nhìn chúng bất cứ khi nào tôi vào “chế độ chỉnh sửa” và chỉ nhìn lại các đạo cụ khi tôi thoát khỏi nó.
Dan Abramov

3
Đối với người mà bạn sẽ sử dụng mã này, React đã đổi tên escapeTextForBrowserđể escapeTextContentForBrowser.
wuct

8

Vì khi chỉnh sửa hoàn tất, tiêu điểm từ phần tử luôn bị mất, bạn có thể chỉ cần sử dụng móc onBlur.

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

5

Tôi đề nghị sử dụng một mutObserver để làm điều này. Nó cho phép bạn kiểm soát nhiều hơn những gì đang diễn ra. Nó cũng cung cấp cho bạn thêm chi tiết về cách trình duyệt diễn giải tất cả các lần nhấn phím

Đây trong 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

Đây là một thành phần kết hợp nhiều điều này của lovasoa: https://github.com/lovasoa/react-contentitable/blob/master/index.js

Anh ta che giấu sự kiện trong ReleaseChange

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

Tôi đang sử dụng một cách tiếp cận tương tự thành công


1
Tác giả đã ghi có câu trả lời SO của tôi trong package.json. Đây gần giống như mã mà tôi đã đăng và tôi xác nhận mã này phù hợp với tôi. github.com/lovasoa/react-contentitable/blob/master/…
Sebastien Lorber
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.