instructure-react / react-tinymce

React TinyMCE component
181 stars 115 forks source link

TinyMCE does not rerender when content property changes #21

Open adiachenko opened 8 years ago

adiachenko commented 8 years ago

Consider the following piece of code.

...
    const {content, setContent} = this.props

    return <div className="field">
      <TinyMCE
        content={content}
...

TinyMCE will ignore the changes of content property and will display only it's value on initial render. I managed to get around this limitation by changing the key property on a parent component from a snippet above (which forces it's re-render) but is there any other way to achieve this without hacking my way through?

buritos commented 8 years ago

Setting a unique property on a parent element works well. Perhaps this could be done internally in the TinyMCE component (some content hash value) to make it behave more inline with what people are used to from using other components.

jonathaningram commented 8 years ago

I'm not sure if it's exactly the correct lifecycle method to use, but if the lib's TinyMCE class method componentWillReceiveProps is updated to this, I've found that it works:

componentWillReceiveProps(nextProps) {
    if (!isEqual(this.props.config, nextProps.config)) {
      this._init(nextProps.config, nextProps.content)
    }
    if (!isEqual(this.props.id, nextProps.id)) {
      this.id = nextProps.id
    }
    // Added
    if (!isEqual(this.props.content, nextProps.content)) {
      tinymce.EditorManager.get(this.id).setContent(nextProps.content)
    }
},
janjakubnanista commented 8 years ago

+1 for the fix from @jonathaningram

albertboada commented 8 years ago

@jonathaningram, your solution would solve your problem, but would cause an unnecessary "re-render" everytime we change the content of the editor using the UI (i.e. typing) in a normal scenario like this one https://github.com/instructure-react/react-tinymce/blob/master/examples/basic/app.js.

How to reproduce:

  1. Type something in the editor. (Note tat at this point, the editor's UI already has the content we want.)
  2. Tinymce fires its own editor.on("change" event, which will fire the onChange handler we have attached to the component via props (onChange={this.handleEditorChange}).
  3. Our handler (handleEditorChange) does a setState.
  4. Since the initial content of the editor was set via content={this.state.content}, and a setState was just fired, componentWillReceiveProps hook will execute.
  5. Since nextProps.content will be different than this.props.content (this.props.content was never updated after typing in the editor), setContent will fire. (Note that this setContent is totally unnecessary)

This unnecessary setContent makes the editor glitchy for an instant, re-positioning the cursor.

How I solved the problem reported in this issue, but avoiding the described glitch at the same time:

comparing nextProps.content with the actual content of the editor

componentWillReceiveProps(nextProps) {
    ...
    if (!isEqual(tinymce.EditorManager.get(this.id).getContent(), nextProps.content)) {
      tinymce.EditorManager.get(this.id).setContent(nextProps.content)
    }
},

This way, the extra setContent call will never fire if the event comes from typing something in the editor's UI.

jonathaningram commented 8 years ago

@albertboada thanks so much for the detail!

I just tried it out and it worked great except for a minor mod that was required for when the editor content was empty to begin with:

// check editor exists before use
const editor = tinymce.EditorManager.get(this.id)
if (editor && !isEqual(editor.getContent(), nextProps.content)) {
  tinymce.EditorManager.get(this.id).setContent(nextProps.content)
}

cc @JasonTolliver who proposed #23

albertboada commented 8 years ago

Why do we need to check if the editor exists. It does, since it's created in the componentDidMount. Plus, it works for me with empty initial content. I'm probably not understanding the issue!

jonathaningram commented 8 years ago

@albertboada yeah not sure, for me it was undefined :confused: may be that I'm doing something unusual, but I can't see what it would be.

image

albertboada commented 8 years ago

Could I see some code example on how you use the component to get that error message?

jonathaningram commented 8 years ago

Note: I only have to load the page to see this happen, don't even need to type.

So I suppose the componentDidReceiveProps is called by something before the editor exists.

Here's how I'm using it:

<TinyMCE
  {...body}
  content={body.value}
  config={{
    plugins: 'autolink link image lists code',
    toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | code',
    image_caption: true
  }}
  onChange={this.handleBodyChange} />

({...body} is from redux-form). But I also tried removing those props and using a string for content and it will errors.

<TinyMCE
  content={'test'}
  config={{
    plugins: 'autolink link image lists code',
    toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | code',
    image_caption: true
  }} />

OK so just tried putting it in a really dumb container so no redux-form, and it was fine with no error. I'll have to try and figure out what part of my form component is causing that to break and I'll try to make a minimum repeatable example and link it up.

albertboada commented 8 years ago

For the record, found a (terrible) bug in my fix. If you init the TinyMCE without passing a content={ } prop, the editor will get emptied everytime you type in it (i.e. everytime the editor.on("change") event fires) :(

jonathaningram commented 8 years ago

Yeah just noticed it, back to the drawing board!

JasonTolliver commented 8 years ago

@albertboada @jonathaningram discovered a small issue with the getContent fix in that you need to explicitly tell it to get the raw content (e.g. getContent({format: 'raw'})) otherwise if there is any html etc TinyMCE might decide to strip attributes out and give you some false positives.

As for that nasty ol' bug when not passing a content={ } prop, it all stems from that getDefaultProps method setting content to an empty string. Without it, we can easily test if nextProps.content is undefined (which means that it was never set or something has gone terribly wrong) and thus not perform the update.

e.g.:

const editor = tinymce.EditorManager.get(this.id);
if (nextProps.content && !isEqual(editor.getContent({format: 'raw'}), nextProps.content)) {
  editor.setContent(nextProps.content);

  editor.selection.select(editor.getBody(), true);
  editor.selection.collapse(false);
}

The undefined equates to an empty string anyway when rendering the DOM elements and give the added benefit of actually knowing when it's undefined lol

@jonathaningram I haven't noticed the need to check for that editor either, but it could very well be an issue

jonathaningram commented 8 years ago

@JasonTolliver thanks for the raw trick, I just noticed in my editor that anytime you press enter for a newline the edior was jumping back to the previous line, despite that it did indeed insert the line. The reason was because in my on change handler I didn't use raw:

handleChange = (e) => {
  const val = e.target.getContent({format: 'raw'})
  // ...
};

Not sure if it's related or if it's just a TinyMCE thing, but it seems to put an &nbsp; at the start of newlines too, which is annoying. I found some Tiny config to change it from inserting <p>s to just inserting <br>s, but that's not what I need, can't see why it needs to put an &nbsp; at the start of the line though.

image

cc @albertboada

trelly commented 7 years ago

how to fixed the bug?

gregpalaci commented 7 years ago

http://archive.tinymce.com/wiki.php/API3:method.tinymce.Editor.setContent

.setContent(content, {format : 'raw'})

davegri commented 7 years ago

Whats going on with this bug? This is a pretty big issue for us because our default value is being loaded async. Is there a way to fix this not internally?

bast44 commented 7 years ago

The alternative library do not have a problem: https://github.com/HurricaneJames/react-tinymce-input

kappuccino commented 7 years ago

If you want to load your default content async this could be usefull

{ !!async-content 
    ? <TinyMCE content={async-content} onChange={ ... } config={{...}} />
       : <p>Loading...</p>
}

wait until you get your content, then render the component with this content pro: no more headache con: you do not see the editor until your content is loaded

shikelong commented 7 years ago

albertboada's method is useful for me.

w- commented 7 years ago

@bast44

The alternative library do not have a problem: https://github.com/HurricaneJames/react-tinymce-input

Thank you for recommending this!

jamie-beck commented 7 years ago

I am moving to https://github.com/HurricaneJames/react-tinymce-input as well over this issue.

RaulTsc commented 7 years ago

I've got the same issue. Any plans on fixing this?

@mzabriskie with some guidance I could do a PR to fix it. What do you think?

Evanlai8 commented 6 years ago

still have this problem when I refresh my content in redux , has this issue been solved in new version?

Aathi commented 6 years ago

it started to work as I expected when I assign a key to TinyMec


import React, {Component} from 'react';
import TinyMCE from 'react-tinymce';

class RichTextEditor extends Component {
    handleEditorChange = (e) => {
        const {input: {
                onChange
            }} = this.props;
        onChange(e.target.getContent());
    }

    render() {
        const { input } = this.props
        return (
            <div>
                <TinyMCE
                    key={input.value} // Assign a key here
                    content={input.value}
                    config={{
                        plugins: [
                          'advlist autolink lists link image charmap print preview anchor',
                          'searchreplace visualblocks code fullscreen',
                          'insertdatetime media table contextmenu paste code',
                        ].join(' '),
                        toolbar: 'undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image',
                        menubar: false,
                        statusbar: false,
                        height: 250,
                    }}
                    onChange={this.handleEditorChange}/>
            </div>
        );
    }
}

RichTextEditor.defaultProps = {
    addField: true
};

export default RichTextEditor;
hatuanbao commented 6 years ago

@Aathi 👍 thanks, it works

daishanxiang commented 6 years ago
    <div style={{display: inputHtml ? 'block' : 'none'}}>
      {inputHtml && <TinyMCE
        content={inputHtml}
        onChange={this.handleEditorChange}
      />}
    </div>
    <div style={{display: inputHtml ? 'none' : 'block'}}>
      <TinyMCE
        content={inputHtml}
        onChange={this.handleEditorChange}
      />
    </div>
shawnmclean commented 6 years ago

Aathi's solution doesn't work for me. Changing the key seems to refresh the editor. The editor loses focus on every key change. Calling input.onChange forces a prop change of the value coming from redux.

shawnmclean commented 6 years ago

Ended up switching to react-tinymce-input. It was quite simple too, just a few property name changes and event args.

itminhnhut commented 3 years ago

opinion: use onEditorChange is not re render. https://codesandbox.io/s/tinymce-react-demo-forked-gg7o8?file=/src/components/editor.js:447-461

shayna-ilya commented 2 years ago

it started to work as I expected when I assign a key to TinyMec

Thanks, works