React-draft-wysiwyg: Cursor resetting position when loading previous contentState

Created on 1 May 2018  路  20Comments  路  Source: jpuri/react-draft-wysiwyg

Hello, I'm experience some strange behavior when loading in a previous content state from my database. The cursor keeps backtracking randomly. Issue is illustrated here:
animation
Here is my rather simple code:

class EditForm extends Component {
    state = {
        editorState: EditorState.createEmpty(),
        contentState: null,
     };
     componentDidMount() {
          fetch("api")
                      .then( res => res.json())
                      .then(data => this.setState({ contentState: JSON.parse(data) })   
     }
    onContentStateChange = contentState => {
     this.setState({
       contentState
       });
    }
     render(){
          return(
       <Editor
                editorState={this.state.editorState}
                contentState={this.state.contentState}
                onContentStateChange={this.onContentStateChange}
              />
          );
       }
  };

I couldn't find any examples in the docs regarding this.

Most helpful comment

I solved it by changing editorState with defaultEditorState

<Editor
    defaultEditorState={editorState}
    wrapperClassName="___editor__wrapper"
    toolbarClassName={cx('___editor__toolbar', { focused })}
    editorClassName={cx('___editor__content', 'form-control', { focused })}
    onEditorStateChange={this.onEditorStateChange}
    onBlur={this.onBlur}
    onFocus={this.onFocus}
    placeholder={placeholder}
    readOnly={readonly}
    toolbar={{
    options: ['inline'],
    inline: {
        options: ['bold', 'italic', 'underline'],
    },
    }}
/>

The cursor problem is gone, but new problem arise, the component cannot recieved initial value even though it was re-rendered

Tnx it's working

All 20 comments

@jonasgroendahl you are using both editorState and contentState props here. They make editor a controlled component. Since you are not updating editor state its messing up.

I would suggest that use only editorState and update it with changes in editor content.

Convert it to contentState only while saving in the DB.

Hello @jpuri, I'm having the same problem
I would like to have controlled editor.

class Editor extends Component {
  onEditorStateChange(editorState) {
    const textHtml = draftToHtml(convertToRaw(editorState.getCurrentContent()));

    this.props.onChange(textHtml);
  }

  render() {
    const contentBlock = htmlToDraft(this.props.value);
    const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
    const editorState = EditorState.createWithContent(contentState);

    return (
      <Editor
        editorState={editorState}
        onEditorStateChange={state => this.onEditorStateChange(state)}
      />
    );
  }
}

In example above redux-form is passing html string through this.props.value.
Any suggestions? Thank you!

Using editorState={editorState} makes editor a controlled component, you need to ensure that you update it for each change in editor, check this: https://github.com/jpuri/react-draft-wysiwyg/tree/master/stories/BasicControlled
Or rather use: https://github.com/jpuri/react-draft-wysiwyg/blob/master/src/Editor/index.js#L50

Also, you can try

import {
  EditorState,
} from "draft-js";
editorState = EditorState.moveSelectionToEnd(editorState);

Issue in this case may be that you are creating editor state each time from content state and content state does not saves selection.

I'm getting the same issue, and when i use EditorState.moveSelectionToEnd the cursor is always moved to the last when editing, so it's not possible to move the cursor back and edit the text it's always being moved to the last position. Any other way to solve this ?

What i'm trying to achieve is to have a controlled editor but in my case i need the content to be changed on componentWillReceiveProps:

   componentWillReceiveProps (nextProps) {
        const nextValue = nextProps.value
        const { value } = this.props
        if (nextValue !== value) {
            const contentBlock = htmlToDraft(nextValue)
            const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)
            const editorState = EditorState.createWithContent(contentState)
            this.setState({
                editorState: editorState
            })
        }
    }

But after adding this code the cursor will move to the first position and start overriding the state. I can't find anything in the examples for an editor that needs to change their editorState when props change.

having Same issue Like @marlonmantilla.

@jpuri have same problem like @marcelometal and @nilaybrahmbhatt

In my case I get value in html-format from props of parent component and should update that value in parents state by 'this.props.onChange'
When I restart page, I get default value with help of 'componentWillReceiveProps' and I can put cursor whenever I want, but it's still moving it to the begining after every button click action
Here is my full component:

`

constructor(props) {
    super(props);
    this.state = {
        editorState: EditorState.createEmpty(),
    };
}

componentWillReceiveProps( nextProps ) {
    const nextValue = nextProps.value
    const { value } = this.props;
    console.log(nextProps.value)
    if (nextValue !== value) {
        const contentBlock = htmlToDraft(nextValue)
        const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)
        const editorState = EditorState.createWithContent(contentState)
        this.setState({
            editorState: editorState
        })
    }
}

onEditorStateChange = (editorState) => {
    let html = draftToHtml( convertToRaw(editorState.getCurrentContent()))

    if ( isFunction(this.props.onChange) ) { this.props.onChange(html) }
    this.setState( { editorState } )
};
render() {
    const { editorState } = this.state;

    return (
        <div>
            <Editor
                editorState={editorState}
                onEditorStateChange={this.onEditorStateChange}
                wrapperClassName="editor-wrapper"
                editorClassName="editor-editor"
                placeholder='Write down your text here ...'
            />
        </div>
    )
}

`

Could you please give us a hand? :)

@KipariS i've found that issues are caused by componentWillReceiveProps (since it's an anti pattern anyway more info here ) and ended up moving things to the constructor instead and using the key attribute to trigger mounting the component:

  // wysiwyg editor component
    constructor(props) {
        super(props)
        const contentBlock = htmlToDraft(props.value || '')
        const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)

        this.state = {
            editorState: EditorState.createWithContent(contentState),
            focused: false
        }
    }

 // parent 
  <WYSIWYGEditor
     key={`editor-${id}`}
     value={value} />

So whenever the id in the key attribute changes React will create a new component instead so you will get things initialized via constructor. Hope that helps!

@marlonmantilla Yep, it's good idea, but when my component mounting and constructor called, it still doesn't have value-data. So I need something to update it after I will receive value prop.

Hi guys, got the same issue but in my case, it only happens if I type fast enough (faster than normal typing), here is my code

import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';

import Wrapper from './Wrapper';

class TextEditor extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: this.parseEditorState(props.value),
      focused: false,
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.value && nextProps.value !== this.props.value) {
      this.setState({ editorState: this.parseEditorState(nextProps.value) });
    }
  }

  onFocus = () => this.setState({ focused: true });
  onBlur = () => this.setState({ focused: false });

  onEditorStateChange = (editorState) => {
    this.setState({ editorState });
    if (this.props.onValueChange) {
      const content = draftToHtml(convertToRaw(editorState.getCurrentContent()));
      this.props.onValueChange(content);
    }
  }

  parseEditorState = (value) => {
    const blocksFromHtml = htmlToDraft(value);
    const { contentBlocks, entityMap } = blocksFromHtml;
    const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
    return EditorState.createWithContent(contentState);
  }

  render() {
    const { placeholder, value, onValueChange, readonly, ...rest } = this.props;
    const { editorState, focused } = this.state;
    return (
      <Wrapper readonly={readonly} {...rest}>
        <Editor
          editorState={editorState}
          wrapperClassName="___editor__wrapper"
          toolbarClassName={cx('___editor__toolbar', { focused })}
          editorClassName={cx('___editor__content', 'form-control', { focused })}
          onEditorStateChange={this.onEditorStateChange}
          onBlur={this.onBlur}
          onFocus={this.onFocus}
          placeholder={placeholder}
          readOnly={readonly}
          toolbar={{
            options: ['inline'],
            inline: {
              options: ['bold', 'italic', 'underline'],
            },
          }}
        />
      </Wrapper>
    );
  }
}

TextEditor.propTypes = {
  placeholder: PropTypes.node,
  value: PropTypes.string,
  onValueChange: PropTypes.func,
  success: PropTypes.any,
  error: PropTypes.any,
  readonly: PropTypes.bool,
};

TextEditor.defaultProps = {
  value: '',
};

export default TextEditor;

Note: Wrapper component just for styling things

I solved it by changing editorState with defaultEditorState

<Editor
    defaultEditorState={editorState}
    wrapperClassName="___editor__wrapper"
    toolbarClassName={cx('___editor__toolbar', { focused })}
    editorClassName={cx('___editor__content', 'form-control', { focused })}
    onEditorStateChange={this.onEditorStateChange}
    onBlur={this.onBlur}
    onFocus={this.onFocus}
    placeholder={placeholder}
    readOnly={readonly}
    toolbar={{
    options: ['inline'],
    inline: {
        options: ['bold', 'italic', 'underline'],
    },
    }}
/>

The cursor problem is gone, but new problem arise, the component cannot recieved initial value even though it was re-rendered

@archansel yes man, with added content from parent of component, it's not working.

I end up using key as @marlonmantilla suggested and it solved my problem

I solved it like this:

on editorState changed:

onEditorStateChange(editorState) {
    this.setState({editorState})
    const content = draftToHtml(convertToRaw(editorState.getCurrentContent())).trim().replace('<p></p>', '')
    this.content = content
    this.props.onContentChange && this.props.onContentChange(content)
}

on componentWillReceiveProps:

componentWillReceiveProps(nextProps) {
    const newState = {}
    if (nextProps.content && nextProps.content !== this.content) {
        this.content = nextProps.content
        newState.editorState = this.createEditorStateFromContent(nextProps.content)
    }
    Object.keys(newState).length > 0 && this.setState(newState)
}

createEditorStateFromContent(content) {
    const contentBlock = htmlToDraft(content)
    const contentState = ContentState.createFromBlockArray(contentBlock)
    return EditorState.createWithContent(contentState)
}

After a lot of effort, i could integrate redux-form with react-draft-wysiwyg loading previus state..

thanks community.

MY COMPONENT

export class LabelAndTextArea extends Component {
constructor(props) {
super(props);
const editorState = EditorState.createEmpty();
this.state = {
editorState
};
this.changeValue(editorState);
}

static defaultProps = {
placeholder: "Seu texto aqui."
};

/**

  • Initialising the value for
    */
    initEditorState() {
    const html = "";
    const contentBlock = htmlToDraft(html);
    const contentState = ContentState.createFromBlockArray(
    contentBlock.contentBlocks
    );
    return EditorState.createWithContent(contentState);
    }

/**

  • This is used by to handle change
    */
    handleChange(editorState) {
    this.setState({ editorState });
    this.changeValue(editorState);
    }

componentWillReceiveProps(nextProps) {
// this loads data from previus state.
const { input } = nextProps;
if (
input.value &&
input.value !== this.props.value &&
input.value !== "

\n"
) {
const contentBlock = htmlToDraft(input.value);
const contentState = ContentState.createFromBlockArray(
contentBlock.contentBlocks
);
const editorState = EditorState.moveFocusToEnd(
EditorState.createWithContent(contentState)
);
this.setState({ editorState });
}
}

onBlur(event) {
const value = draftToHtml(
convertToRaw(this.state.editorState.getCurrentContent())
);
this.props.input.onBlur(value);
}

/**

  • This updates the redux-form wrapper
    */
    changeValue(editorState) {
    const value = draftToHtml(convertToRaw(editorState.getCurrentContent()));
    this.props.input.onChange(value);
    }

render() {
const { editorState } = this.state;
const { cols, name, label, placeholder, meta } = this.props;
return (

editorState={editorState}
name={name}
wrapperClassName="border rounded"
editorClassName="ml-2"
className="form-control"
placeholder={placeholder}
onBlur={event => this.onBlur(event)}
onEditorStateChange={editorState => this.handleChange(editorState)}
// how to config: https://jpuri.github.io/react-draft-wysiwyg/#/docs

      toolbar={{
        options: ["inline", "list"],
        inline: {
          inDropdown: false,
          options: ["bold", "italic", "underline", "strikethrough"]
        },
        list: { inDropdown: false, options: ["unordered", "ordered"] }
      }}
    />
    <ErrorAlert meta={meta} />
  </Grid>
);

}
}

USAGE
component={LabelAndTextArea}
label="Motivo"
name="motivo"
validate={[textAreaRequired]}
/>

I solved it like this:

on editorState changed:

onEditorStateChange(editorState) {
    this.setState({editorState})
    const content = draftToHtml(convertToRaw(editorState.getCurrentContent())).trim().replace('<p></p>', '')
    this.content = content
    this.props.onContentChange && this.props.onContentChange(content)
}

on componentWillReceiveProps:

componentWillReceiveProps(nextProps) {
    const newState = {}
    if (nextProps.content && nextProps.content !== this.content) {
        this.content = nextProps.content
        newState.editorState = this.createEditorStateFromContent(nextProps.content)
    }
    Object.keys(newState).length > 0 && this.setState(newState)
}

createEditorStateFromContent(content) {
    const contentBlock = htmlToDraft(content)
    const contentState = ContentState.createFromBlockArray(contentBlock)
    return EditorState.createWithContent(contentState)
}

there also has problem ,when i type fast ,

After a lot of effort, i could integrate redux-form with react-draft-wysiwyg loading previus state..

thanks community.

MY COMPONENT

export class LabelAndTextArea extends Component {
constructor(props) {
super(props);
const editorState = EditorState.createEmpty();
this.state = {
editorState
};
this.changeValue(editorState);
}

static defaultProps = {
placeholder: "Seu texto aqui."
};

/**

  • Initialising the value for
    */
    initEditorState() {
    const html = "";
    const contentBlock = htmlToDraft(html);
    const contentState = ContentState.createFromBlockArray(
    contentBlock.contentBlocks
    );
    return EditorState.createWithContent(contentState);
    }

/**

  • This is used by to handle change
    */
    handleChange(editorState) {
    this.setState({ editorState });
    this.changeValue(editorState);
    }

componentWillReceiveProps(nextProps) {
// this loads data from previus state.
const { input } = nextProps;
if (
input.value &&
input.value !== this.props.value &&
input.value !== "

\n"
) {
const contentBlock = htmlToDraft(input.value);
const contentState = ContentState.createFromBlockArray(
contentBlock.contentBlocks
);
const editorState = EditorState.moveFocusToEnd(
EditorState.createWithContent(contentState)
);
this.setState({ editorState });
}
}
onBlur(event) {
const value = draftToHtml(
convertToRaw(this.state.editorState.getCurrentContent())
);
this.props.input.onBlur(value);
}

/**

  • This updates the redux-form wrapper
    */
    changeValue(editorState) {
    const value = draftToHtml(convertToRaw(editorState.getCurrentContent()));
    this.props.input.onChange(value);
    }

render() {
const { editorState } = this.state;
const { cols, name, label, placeholder, meta } = this.props;
return (

editorState={editorState}
name={name}
wrapperClassName="border rounded"
editorClassName="ml-2"
className="form-control"
placeholder={placeholder}
onBlur={event => this.onBlur(event)}
onEditorStateChange={editorState => this.handleChange(editorState)}
// how to config: https://jpuri.github.io/react-draft-wysiwyg/#/docs

      toolbar={{
        options: ["inline", "list"],
        inline: {
          inDropdown: false,
          options: ["bold", "italic", "underline", "strikethrough"]
        },
        list: { inDropdown: false, options: ["unordered", "ordered"] }
      }}
    />
    <ErrorAlert meta={meta} />
  </Grid>
);

}
}

USAGE

do you have any other input except rich editor,eg input ????when i use EditorState.moveFocusToEnd,
it always stay in the rich editor end ,no longer focus in the upper input box
image

I solved it by changing editorState with defaultEditorState

<Editor
    defaultEditorState={editorState}
    wrapperClassName="___editor__wrapper"
    toolbarClassName={cx('___editor__toolbar', { focused })}
    editorClassName={cx('___editor__content', 'form-control', { focused })}
    onEditorStateChange={this.onEditorStateChange}
    onBlur={this.onBlur}
    onFocus={this.onFocus}
    placeholder={placeholder}
    readOnly={readonly}
    toolbar={{
    options: ['inline'],
    inline: {
        options: ['bold', 'italic', 'underline'],
    },
    }}
/>

The cursor problem is gone, but new problem arise, the component cannot recieved initial value even though it was re-rendered

Tnx it's working

when create new EditorState from content, the SelectionState is re-created, and the cursor position information (anchoroffset, focusOffset) will not kept, so need to update from old editorState.

for the EditorState.forceSelection and Editor.acceptSelection can refer to the link

  static getDerivedStateFromProps(nextProps, state) {

    if ('value' in nextProps) {

      // get old editor state and old editor selection
      let oldEditorState = state.editorState
      const oldSelectionState = oldEditorState.getSelection();

      // create new editorState from content
      let content = nextProps.value 
      let contentBlock = htmlToDraft(content || "")
      const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
      const newEditorState = EditorState.createWithContent(contentState);

      // update cursor position base on old selection state
      let updateSelection = newEditorState.getSelection().merge({
        anchorOffset: oldSelectionState.getAnchorOffset(),
        focusOffset: oldSelectionState.getFocusOffset(),
        isBackward: false,
      })

      // check whether editor on focus, if onforcus use forceSelection to manually update the posiition
      // if not on focus, use acceptSelection
      let newEditorStateWithSelection;
      if (oldSelectionState.getHasFocus()) {
        newEditorStateWithSelection = EditorState.forceSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      } else {
        newEditorStateWithSelection = EditorState.acceptSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      }

      return {
        editorState: newEditorStateWithSelection
      };
    }
  }

  onEditorStateChange = editorState => {
    let value = draftToHtml(convertToRaw(editorState.getCurrentContent()))
    this.setState({ editorState });
  }

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Fireprufe15 picture Fireprufe15  路  4Comments

Pixelatex picture Pixelatex  路  3Comments

dahudson88 picture dahudson88  路  3Comments

kcabading picture kcabading  路  3Comments

june50232 picture june50232  路  4Comments