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.inputHandlerto listen for changes, figure out the new state of the editor content, dispatch action to update state with new content, thenreturn falseto prevent other event handlers triggering - in effect - so it doesn’t update the actual editor via this. - Also looked into
EditorView.domEventHandlersthough 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.