React.js : contentEditable에 대한 onChange 이벤트


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
이 문제로 어려움을 겪고 제안 된 답변에 문제가 있었기 때문에 대신 제어하지 않기로 결정했습니다. 즉, 내가 넣어 initialValuestate하고 그것을 사용 render,하지만 난 더 업데이 트를이 반응하지 않습니다.
Dan Abramov 2014 년

귀하의 JSFiddle이 작동하지 않습니다
녹색

나는 contentEditable접근 방식을 변경 하여 어려움을 피 했습니다. span또는 대신을 속성 과 함께 paragraph사용했습니다 . inputreadonly
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

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"}})
산적

1
니스는하지만 전화를 추측 this.getDOMNode().innerHTMLshouldComponentUpdate매우 잘 최적화되어 있지 않습니다
세바스티앙 Lorber을

@SebastienLorber는 매우 최적화 되지 않았지만 HTML을 설정하는 것보다 읽는 것이 더 낫다고 확신합니다. 내가 생각할 수있는 유일한 다른 옵션은 html을 변경할 수있는 모든 이벤트를 수신하는 것입니다. 이러한 이벤트가 발생하면 html을 캐시합니다. 대부분의 경우 속도가 더 빠르지 만 많은 복잡성을 추가합니다. 이것은 매우 확실하고 간단한 해결책입니다.
Brigand 2015-06-28

3
이것은 실제로 state.html마지막 "알려진"값 으로 설정하고 싶을 때 약간 결함이 있습니다 . React는 새 html이 React에 관한 한 정확히 동일하기 때문에 DOM을 업데이트하지 않습니다 (실제 DOM은 다르더라도). jsfiddle을 참조하십시오 . 이에 대한 좋은 해결책을 찾지 못 했으므로 어떤 아이디어라도 환영합니다.
univerio 2014-06-29

1
@dchest shouldComponentUpdate는 순수해야합니다 (부작용이 없음).
Brigand

66

2015 년 수정

누군가 내 솔루션으로 NPM에 프로젝트를 만들었습니다 : https://github.com/lovasoa/react-contenteditable

편집 06/2016 : 브라우저가 방금 제공 한 html을 "재 포맷"하려고 할 때 발생하는 새로운 문제가 발생하여 구성 요소가 항상 다시 렌더링됩니다. 보다

2016 년 7 월 편집 : 여기 내 프로덕션 콘텐츠 편집 가능한 구현입니다. 다음을 react-contenteditable포함하여 원하는 추가 옵션 이 있습니다.

  • 잠금
  • HTML 조각을 포함 할 수있는 명령형 API
  • 콘텐츠를 다시 포맷하는 기능

요약:

FakeRainBrigand의 솔루션은 새로운 문제가 발생할 때까지 한동안 꽤 잘 작동했습니다. ContentEditables는 고통스럽고 React를 다루기가 정말 쉽지 않습니다 ...

JSFiddle 은 문제를 보여줍니다.

보시다시피 일부 문자를 입력하고을 클릭 Clear하면 내용이 지워지지 않습니다. 이는 contenteditable을 마지막으로 알려진 가상 DOM 값으로 재설정하려고하기 때문입니다.

따라서 다음과 같이 보입니다.

  • shouldComponentUpdate캐럿 위치 점프를 방지 해야 합니다.
  • shouldComponentUpdate이런 식으로 사용하면 React의 VDOM diffing 알고리즘에 의존 할 수 없습니다 .

따라서 shouldComponentUpdate예를 반환 할 때마다 DOM 콘텐츠가 실제로 업데이트되었는지 확인할 수 있도록 추가 줄이 필요합니다 .

따라서 여기 버전은 a를 추가하고 다음 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를 넣으면 contenteditable이 다시 렌더링되지 않습니다 (적어도 키 입력시)

2) 구성 요소가 키 입력시 다시 렌더링되지 않으면 React는이 콘텐츠에 대한 오래된 가상 돔을 편집 가능하게 유지합니다.

3) React가 가상 DOM 트리에서 오래된 버전의 contenteditable을 유지하는 경우 가상 DOM에서 오래된 값으로 contenteditable을 재설정하려고하면 가상 DOM diff 중에 React는 변경 사항이 없다고 계산합니다. DOM에 적용하십시오!

이것은 주로 다음과 같은 경우에 발생합니다.

  • 처음에 비어있는 contenteditable이 있습니다 (반드시 ComponentUpdate = true, prop = "", previous vdom = N / A),
  • 사용자가 일부 텍스트를 입력하고 렌더링을 방지합니다 (shouldComponentUpdate = false, prop = text, previous vdom = "").
  • 사용자가 유효성 검사 버튼을 클릭 한 후 해당 필드를 비워야합니다 (shouldComponentUpdate = false, prop = "", previous vdom = "").
  • 새로 생성 된 vdom과 기존 vdom이 모두 ""이므로 React는 dom을 건드리지 않습니다.

1
Enter 키를 눌렀을 때 텍스트를 알리는 keyPress 버전을 구현했습니다. jsfiddle.net/kb3gN/11378
Luca Colonnello

@LucaColonnello 당신은 더 나은 사용하십시오 {...this.props}클라이언트가 외부에서이 동작을 사용자 정의 할 수 있도록
세바스티앙 Lorber

오 그래,이게 더 낫다! 솔직히 저는 keyPress 이벤트가 div에서 작동하는지 확인하기 위해서만이 솔루션을 시도했습니다!
설명해

1
shouldComponentUpdate코드가 캐럿 점프를 어떻게 방지 하는지 설명해 주 시겠습니까?
kmoe

1
@kmoe는 contentEditable에 이미 적절한 텍스트가있는 경우 (즉, 키 입력시) 구성 요소가 업데이트되지 않기 때문입니다. React로 contentEditable을 업데이트하면 캐럿이 점프합니다. 의 contentEditable없이 시도하고 자신을 참조)
세바스티앙 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
React 상태로 텍스트를 업데이트하면 캐럿이 텍스트 시작 부분으로 계속 이동합니다.
Juntae

18

이것은 아마도 당신이 찾고있는 대답이 아닐 수도 있지만,이 문제로 어려움을 겪고 제안 된 대답에 문제가있어서 대신 통제하지 않기로 결정했습니다.

editable소품은 false, 내가 사용 text이기 때문에 소품을하지만,이 때 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쉽게 구현할 수 없으므로 더 이상 캐럿 점프에서 나를 구할 수 없습니다. 마지막으로 CSS 의사 요소 :empty:before자리 표시자가 있지만이 shouldComponentUpdate구현으로 인해 사용자가 필드를 지울 때 FF와 Safari가 필드를 정리하지 못했습니다. 통제되지 않은 CE로 이러한 모든 문제를 피할 수 있다는 사실을 깨닫는 데 5 시간이 걸렸습니다.
Dan Abramov 2014 년

어떻게 작동하는지 잘 모르겠습니다. 당신 editableUncontrolledContentEditable. 실행 가능한 예제를 제공 할 수 있습니까?
NVI

@NVI : 여기서 React 내부 모듈을 사용하기 때문에 조금 어렵습니다. 기본적으로 editable외부에서 설정 했습니다. 사용자가 "편집"을 누를 때 인라인으로 편집 할 수 있고 "저장"또는 "취소"를 누를 때 다시 읽기 전용이어야하는 필드를 생각해보십시오. 그래서 읽기 전용 일 때는 소품을 사용하지만 "편집 모드"에 들어갈 때마다보기를 멈추고 종료 할 때만 소품을 다시 봅니다.
Dan Abramov

3
이 코드를 사용할 사람을 위해 React의 이름 escapeTextForBrowserescapeTextContentForBrowser.
wuct

8

편집이 완료되면 요소의 포커스가 항상 손실되므로 onBlur 후크를 사용하면됩니다.

<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에서 이벤트를 shims합니다.

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
저자는 package.json에서 내 SO 답변을 인정했습니다. 이것은 제가 게시 한 코드와 거의 동일하며이 코드가 저에게 맞는지 확인합니다. github.com/lovasoa/react-contenteditable/blob/master/...
세바스티앙 Lorber에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.