/**
- This component allows the user to show and edit the code of html, css, javascript and json.
- It supports code formatting, indentation, syntaxHighlighting and many more.
*/
import React, { useRef, useEffect, useState, forwardRef } from ‘react’;
import classNames from ‘classnames’;
import { EditorState, type Extension } from ‘@codemirror/state’;
import { basicSetup } from ‘codemirror’;
import { EditorView, ViewUpdate, placeholder } from ‘@codemirror/view’;
import { syntaxHighlighting, defaultHighlightStyle } from ‘@codemirror/language’;
// Interfaces
export interface CodeEditorProps {
value?: string;
onChange?: ((value: string) => void);
onBlur?: ((value: string) => void);
}
export interface CodeEditorRef {
formatCode: () => void;
}
// eslint-disable-next-line max-lines-per-function
function CodeEditorComponent({
value = ‘’,
onChange = undefined,
onBlur = undefined,
}: CodeEditorProps, ref: React.Ref): React.ReactElement {
// States
const [code, setCode] = useState(value);
// Refs
const editorRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
const editorViewRef: React.MutableRefObject<EditorView | null> = useRef(null);
useEffect(() => {
if (code !== value) {
onChange?.(code);
}
}, [code]);
/** Lifecycle method to create the editor on the first render with the provided props data. */
useEffect(() => {
const onUpdate: Extension = EditorView.updateListener.of((update: ViewUpdate) => {
const updatedCode = update.state.doc.toString();
setCode(updatedCode);
});
const startState = EditorState.create({
doc: code,
extensions: [
EditorView.editorAttributes.of({
class: classNames('bg-white')
}),
placeholder('// Write your code here...'),
EditorView.theme({
'&': { border: '1px solid silver' },
'.cm-scroller': { overflow: 'auto', 'scrollbar-width': 'thin' },
'.cm-editor': { border: '1px solid black' },
'.cm-selectionBackground, .cm-editor::selection': {
'background-color': '#597fd9 !important'
},
}),
basicSetup,
EditorView.lineWrapping,
syntaxHighlighting(defaultHighlightStyle),
onUpdate,
// EditorView.domEventHandlers({ blur: handleBlur }),
],
});
const view: EditorView = new EditorView({ state: startState, parent: editorRef.current as Element });
editorViewRef.current = view;
view.contentDOM.addEventListener('blur', handleBlur);
return () => {
view.destroy();
};
}, []);
/**
* Function to dispatch the editor function.
* This function is created in order to avoid the editor view check on all the functions.
*/
function dispatchEditorFn(fn: (editorView: EditorView) => void): void {
const editorView: EditorView | null = editorViewRef?.current;
if (editorView) {
fn(editorView);
}
}
function triggerOnBlurCallback(editorView: EditorView): void {
const codeString: string = editorView.state.doc.toString();
onBlur?.(codeString);
}
function handleBlur(): void {
dispatchEditorFn(triggerOnBlurCallback);
}
return (
<div
ref={editorRef}
>
</div>
);
}
const CodeEditor = forwardRef(CodeEditorComponent);
export default React.memo(CodeEditor);
Here is my component sample code, I provide the onBlur callback so that user can perform some action. I tried add event listener to the contentDOM as well but nothing seems to work.
The way I consume the component.
App.tsx
const [value, setValue] = useState(10);
<Button onClick={() => setValue(20)}>Change state
<CodeEditor onBlur={() => {hitApiWithPayload(value)}; console.log("current value: ", value); />
Steps to reproduce
- click on editor and then click outside.
==> result: console==> current value 10
- Click change state button.
- Again click inside the editor and again outside.
==> result: console==> current value 10 (wrong result the value should be 20 here)