Is there a recommended or suggested workflow for using cm/next with React? I’ve been tinkering around with it a lot, and love the fact it makes bundling CM in with my Electron app a breeze without having to worry about externals etc with Webpack.
So to work well with React, I’m exploring the flow required with a state and a reducer, so that on each change, an action is dispatched keeping the reducer state in sync always.
The basic flow would be:
- Setup an Editor initially when a component first renders.
- Set the initial content of the editor
- Listen for change events, and dispatch that state change to the reducer, without directly updating the editor content at this point
- When the component re-renders based on that state change, for the content to be updated, with the cursor remaining in the correct place
My current implementation is something like the following:
class Editor extends React.Component {
constructor(props) {
super(props);
this.codeMirror = React.createRef();
}
componentDidMount() {
this.editor = new EditorView({
state: EditorState.create({
doc: this.props.content,
extensions: [
basicSetup,
javascript(),
],
}),
parent: this.codeMirror.current
});
}
render() {
if (this.editor) {
this.editor.setState(EditorState.create({doc: this.props.content}));
}
return (
<div ref={this.codeMirror} className="editor"></div>
);
}
}
For number 3 - detecting editor changes and dispatching an action, I’ve experimented with a few options:
- Using
EditorView.inputHandler
to listen for changes, figure out the new state of the editor content, dispatch action to update state with new content, thenreturn false
to prevent other event handlers triggering - in effect - so it doesn’t update the actual editor via this. - Also looked into
EditorView.domEventHandlers
though I can’t see a way to simply work out the new state of the editor content without the editor itself being updated.
componentDidMount() {
let listenerExtension = EditorView.inputHandler.of((view, from, to, text) => {
let transaction = view.state.update({
changes: {
from,
to,
insert: text
}
});
this.props.changeEvent(transaction.newDoc.toString()); // this props function dispatches an action
return true;
});
this.editor = new EditorView({
state: EditorState.create({
doc: this.props.content,
extensions: [
basicSetup,
javascript(),
listenerExtension,
],
}),
parent: this.codeMirror.current
});
}
With both of the possible options above, one thing I’ve noticed is that if using backspace or delete for example, the handler isn’t triggered at all.
For number 4, re-rendering of the editor content when props have updated, in the render
method, I need a way to just update the content, without replacing the entire state, as that will also reset extensions unless I explicitly state them again, and also preserving the cursor position too.
One possibility is to have something like:
render() {
if (this.editor) {
let transaction = this.editor.state.update({
changes: {
from: 0,
to: this.editor.state.doc.toString().length,
insert: this.props.content
}
});
this.editor.dispatch(transaction);
}
return (
<div ref={this.codeMirror} className="editor" onClick={this.props.mouseListener}></div>
);
}
However, cursor position isn’t maintained, and it seems like styling isn’t applied to the changes either.
If anyone has a suggested way for this workflow, or any alternative ways of thinking about usage of CodeMirror/next with React, I’d be really keen to hear from you. Thank you.