draft-js
draft-js copied to clipboard
How to force re-render of Editor
Does anybody know how to force a re-render of the Editor, in particular when you have a custom blockRendererFn
. Note that I want to force a re-render even though the EditorState has not changed. My custom blockRendererFn
depends on state outside the EditorState during its render.
- I can get a re-render to work by using
forceSelection
but then I end up with a selection. I don't want the selection, just the re-render. I also don't necessarily want focus. - I tried using
forceUpdate()
on the Editor component but it is not re-rendering. I'm guessing this is because the EditorState has not changed.
Note: I'm asking this question as a workaround to not having a custom shouldComponentUpdate
as per my issue here https://github.com/facebook/draft-js/issues/457 and related to https://github.com/facebook/draft-js/issues/267
Ignore the close/open of the issue above. Hit the wrong button.
I did find a workaround that is ugly as sin but does work.
The editor will re-render if the decorator changes between states. What I did was instead of creating a decorator to style the Editor, I created a function that creates a decorator to style the editor.
The subtle difference is that whenever I call the function, I get a new instance of the decorator even though it does the exact same thing.
Because it is a different instance, it fails the strict equality check in DraftJS's own shouldComponentUpdate
and hence re-renders.
I made a function like this in the React component that contains the editor to force the update using the createDecorator
method:
forceRender: function () {
var editorState = this.state.editorState;
var content = editorState.getCurrentContent();
var newEditorState = EditorState.createWithContent(content, createDecorator());
this.setState({editorState: newEditorState});
},
It works but I still would prefer a proper solution where we aren't always having to send this forceRender
command instead of just checking to see if the contents have changed using a custom shouldComponentUpdate as per this:
https://github.com/facebook/draft-js/issues/457
Seconded. I'm rendering equations inline using Entities over a single character. Calling Entity.mergeData(...)
to update the equation when it's modified in an editor actually updates the editorState the way I would expect it to, but the Entity component isn't updated.
+1 on this (and thanks for the hacky workaround), i'm modifying text as the user types and my editorState is showing the replaced text but my UI still isn't, until I force a re-render.
+1 this is especially important if u do magic things in the blockStyleFN which only get's called on a re-render
@afraser I have similar issue. Have you found any even hacky solution to this ever since you posted here? I do have inline TeX entities and using mergeData the data associated with the entity is indeed updated but the editor is still showing old value.
@kevinguard @maerzhase @jc4p I'm still manually forcing the update of the equation. It's hacky but not terrible since I couple forceUpdateEquation()
with the InlineMath
component.
Here's the usage in our editor
import InlineMath, { forceUpdateEquation } from 'components/InlineMath'
...
handleSaveEquation(tex) {
const { currentEquationEntityKey } = this.state
if (currentEquationEntityKey) {
// The user is editing an equation that's already in the editor
Entity.mergeData(currentEquationEntityKey, {text: tex})
forceUpdateEquation(currentEquationEntityKey)
} else {
// The user is editing a new equation
this.insertEquation(tex)
}
this.hideEquationEditor()
}
...
insertEquation(tex) {
const editorState = this.state.editorState
const currentContent = editorState.getCurrentContent()
const entity = Entity.create('equation', 'IMMUTABLE', { text: tex })
const selection = editorState.getSelection()
const textWithEntity = Modifier.replaceText(currentContent, selection, " ", null, entity)
this.setState({
editorState: EditorState.push(editorState, textWithEntity, "insert-characters")
})
}
and here's the inline_math component. I suppose we should be using redux instead of events here, but you get the idea...
import React, { Component, PropTypes } from 'react'
import { Entity } from 'draft-js'
import { trigger, on, off } from '../../utils/events'
//
// Renders an equation entity as InlineTex and binds events neccessary for syncing it
//
export default class InlineMath extends Component {
static propTypes = {
entityKey: PropTypes.string.isRequired,
documentKey: PropTypes.string.isRequired,
onClick: PropTypes.func,
}
getId() {
return `${this.props.entityKey}`
}
componentDidMount() {
on(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())
}
componentWillUnmount() {
off(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())
}
onClick() {
this.props.onClick(this.props.entityKey)
}
render() {
const { text } = Entity.get(this.props.entityKey).getData()
return (
<InlineTex
id={this.getId()}
data-document-key={this.props.documentKey}
onClick={::this.onClick}
contentEditable={false}
offsetKey={ this.props.offsetKey }
tex={text} />
)
}
}
//
// Renders tex via Mathjax
//
export class InlineTex extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
tex: PropTypes.string.isRequired,
}
render() {
const { id, tex, offsetKey, contentEditable, ...props } = this.props
//show the latex symbol as an empty state
let showableTex = tex ? ("$" + tex + "$") : "$\\LaTeX$"
return (
<span className='process_math' data-offset-key={offsetKey} contentEditable={ contentEditable }>
<span data-text='false' id={id} {...props}>{ showableTex }</span>
</span>
)
}
}
export function forceUpdateEquation(entityKey) {
trigger(`update-equation-${entityKey}`)
}
@afraser Thanks a bunch for sharing the snippet. So essentially you are invoking a method on the component and looks like the method basically doesn't do much but just triggering an event. To me it looks like you're forcing the component to re-render itself, right? Did I understand correctly? Thanks a lot for help.
I have the problem too. Editor.forceUpdate()
not trigger the render()
of custom atomic block Component.
@kevinguard Yes, the event is triggered there and listened to by this line:
on(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())
Note that each unique equation binds to it's own unique event name so forceUpdate()
will only be invoked on the equation that needs updating.
@jan4984 @kevinguard I'm using MathJax with this config:
MathJax.Hub.Config({
extensions: ["tex2jax.js"],
jax: ["input/TeX", "output/HTML-CSS"],
tex2jax: {
ignoreClass: "ignore_math",
processClass: "process_math",
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
processEscapes: true,
}
});
Note the process_math
class. That's the same class that's rendered on my InlineMath
component to make sure it's automatically rendered by mathjax after it's rendered by React.
Thank you very much @afraser. I got it working using React/Redux after learning how you do that using events.
@afraser Just one more question. Have you noticed weird cursor behavior immediately after updating the entity? I am also using Entity.mergeData(...)
plus ensuring that when going to mutate the entity content, the editor is set to readOnly mode. Indeed updating the inline TeX works as now I am doing it at the component level but then the cursor jumps around and if I enter something, tons of duplicated characters appear in random places. The workaround is to save the document and then make it editable again which is a terrible user experience. Have you noticed this?
@kevinguard It sounds like the problem you're having is related to the entity being associated with more than one character in the actual document. I'm pretty sure we just decorate a single space (" ") with the equation entity. This will get you most of the way there but there is still another hurdle to jump when an equation happens to fall at the end of a block. The way we handle THAT isn't 100% perfect so I'd rather not share that code just yet.
Thanks @afraser. Totally understand that. I actually took a look at the overall code you posted here and just realized that you are using mathjax
. I am using KaTeX
and that requires rendering as a dangerously formatted string as html. Like you mentioned I am indeed using Modifier.insertTexT()
using a single space ' '
. I suspect the span is goofing things up when I edit and the entity offset just gets messed up. Still working on it.
I had a similar use case where my decorator needs to use a setting external to the EditorState, that should not be affected by undo/redo. When that setting changed I could not get Draft JS to fully re-render, not by forcing selection, or even after creating a new state and setting a new decorator instance.
My ugly solution was to specify a React key on the Editor component and change it every time my setting changed, forcing react to reinstantiate the editor. Works great, but feels gross.
One way to force an Editor to re-render that isn't too bad is to force a selection:
const forcedState = EditorState.forceSelection(editorState, editorState.getSelection());
return (
<Editor
editorState={someValueChanged ? forcedState : editorState}
/>
);
Apparently even if the selection applied is the same as the existing selection, it treats it as a candidate for re-rendering.
@philraj that seems easy, straight-forward and not so bad, but doesn't do it for me. I need it to re-scan and re-apply decorators, which it doesn't do.
@gbbr Ah too bad. In my case I just needed to re-apply custom styles. I guess the simplest solution in that case is the one by @thesunny. Real shame though.
Also had to use @thesunny's hack to get an entity to update too. I'd tried force selection to get it to re-render, but this doesn't do the trick as @gbbr said.
i am new to draftjs .can anyone help of how can we add inline math formula in draftjs
@gmchaturvedi1 inline custom block only supported if the block only render span
Shall I change div to span in tex example On 16 May 2017 09:07, "JiangYD" [email protected] wrote:
@gmchaturvedi1 https://github.com/gmchaturvedi1 inline custom block only supported if the block only render span
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/draft-js/issues/458#issuecomment-301666519, or mute the thread https://github.com/notifications/unsubscribe-auth/AKWBKZGm2F1HtDe9jUfy9k_nLo7YiVI2ks5r6RnqgaJpZM4IzWAD .
I use this to force render everything and still keep my selection state and undo / redo history:
forceRender: function () {
var editorState = this.state.editorState;
var contentState = editorState.getCurrentContent();
var newEditorStateInstance = EditorState.createWithContent(contentState, yourDecoratorStrategy());
var copyOfEditorState = Draft.EditorState.set(
newEditorStateInstance,
{
selection: editorState.getSelection(),
undoStack: editorState.getUndoStack(),
redoStack: editorState.getRedoStack(),
lastChangeType: editorState.getLastChangeType()
}
);
this.setState({editorState: copyOfEditorState});
},
One of the workarounds above should do the trick here. Please reopen if there are further questions.
@thesunny The Workaround you are providing works fine in terms of forcing the editor to re-render but still no the best performant way to achieve and Entity data merge or replace
The workaround is not perfomant on larger content like 500 blocks (paragraphs) also where each word is an entity.
I use draft.js for editing transcripts and each word has data like start-time and duration, hence having an entity for each word.
Why is this closed?
I wish there was a feature like forceUpdate()
Re-opening this issue cause yeah this seems only doable in a hacky way right now. Exposing some forceUpdate
also seems non-ideal— it's not idiomatic React to tell your element when to re-render.
I'm thinking one of two things could be done here:
- If we see there's any block to be rendered by a custom component, always re-render. This essentially means doing this in the content's
shouldComponentUpdate
:
const nextBlocksAsArray = nextProps.editorState
.getCurrentContent()
.getBlocksAsArray();
for (let ii = 0; ii < nextBlocksAsArray.length; ii++) {
const block = nextBlocksAsArray[ii];
const customRenderer = nextProps.blockRendererFn(block);
if (customRenderer) {
return true;
}
}
This means blockRendererFn
would be called on every render first to check and then to render. Custom block components would always re-render. We'll still try to be efficient about when we re-render DraftEditorBlock
s, but it'll be up to the user to be efficient about their custom blocks.
- If we see the entity map changed, always re-render. This essentially means doing this in the content's
shouldComponentUpdate
:
const nextEntityMap = nextEditorState.getCurrentContent().getEntityMap();
const prevEntityMap = prevEditorState.getCurrentContent().getEntityMap();
if (nextEntityMap !== prevEntityMap) {
return true;
}
The entity API is a little awkward right now, but I think it makes sense for all data rendered by Draft.js blocks to go through entities. Custom block components and blockRendererFn
wouldn't be called if entities don't change, and this potentially lets draft memoize custom block components for you, so that only the blocks whose data changes attempt a re-render.
Either way this will move re-render optimization away from DraftEditorContents
and into individual blocks, which I think is the right move. Choosing to re-render or not at such a high level doesn't really make sense. I'm leaning towards number 2 as the right solution.
Following up on my previous comment: I'm still leaning mostly towards option 2. It seems to be blocked on the incomplete migration out of DraftEntity
that was set to be done by v0.11.0 (oops), so I'll try to get that out by v0.12.0.
Note, this does mean data for custom blocks has to come from entities. I do think this is the right architecture for this. Perhaps the API for blockRendererFn
should be changed so it's not a component, but an instance that gets passed into DraftEditor
too (kind of how children
works), but in any case, I don't think a function to force updates is the move here.
Bumped into this too, more dynamic custom block styles (e.g. one that depends on caret location) are impossible without more granular rendering.
Following your idea but a little less hacky I feel is:
React.useEffect(() => {
setEditorState(
EditorState.forceSelection(editorState, editorState.getSelection())
)
}, [depsThatForceReRender])