Interactive Code Editor Tutorial: Managing Dynamic Content with react-codemirror

mrsupercraft

MrSuperCraft

Posted on October 30, 2024

Interactive Code Editor Tutorial: Managing Dynamic Content with react-codemirror

Building an Interactive Code Editor with React and CodeMirror

Creating a fully interactive code editor in a web app can feel like a complex task. However, with the right tools, you can build a responsive, user-friendly editor that manages dynamic content seamlessly. In this tutorial, I’ll walk you through building a code editor using @uiw/react-codemirror — a powerful, customizable tool that makes setting up syntax highlighting, real-time content changes, and editor configurations straightforward.

Why Share This?

I encountered various challenges while developing my own project, CodeLib, particularly due to verbose documentation on the CodeMirror site, which can be hard to follow for straightforward use cases. With this tutorial, I hope to simplify the process for anyone implementing code editors in their projects.

What You'll Learn

  1. Setting up CodeMirror in a React/Next.js application.
  2. Customizing the editor with syntax highlighting and theme support.
  3. Managing dynamic content updates in the editor.

Requirements

This tutorial uses Next.js, but the setup is suitable for any React-based framework. You’ll need some familiarity with React and TypeScript.


Step 1 - Setting Up CodeMirror

To start, we’ll create a basic code editor component using @uiw/react-codemirror, which simplifies setup compared to react-codemirror. First, install the necessary packages:

npm install @uiw/react-codemirror @codemirror/lang-javascript
Enter fullscreen mode Exit fullscreen mode

Then, create a new component and import CodeMirror:

import CodeMirror from "@uiw/react-codemirror";

const CodeEditor = () => {
  return (
    <CodeMirror height="600px" theme="dark" />
  );
};

export default CodeEditor;
Enter fullscreen mode Exit fullscreen mode

This provides a basic editor which we’ll customize.

Step 2 - Adding Syntax Highlighting

To add syntax highlighting for different languages, import language modules and define extensions:

import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { markdown } from '@codemirror/lang-markdown';

const languageExtensions = {
    js: javascript(),
    python: python(),
    html: html(),
    css: css(),
    markdown: markdown()
};
Enter fullscreen mode Exit fullscreen mode

Define a helper function to match file extensions to languages:

const getLanguageExtension = (filename: string) => {
    const extension = filename.split('.').pop();
    return languageExtensions[extension || ''] || javascript();
};
Enter fullscreen mode Exit fullscreen mode

Then pass the correct language extension to CodeMirror:

<CodeMirror extensions={[getLanguageExtension(filename)]} theme="dark" />
Enter fullscreen mode Exit fullscreen mode

Step 3 - Handling Dynamic Content Updates

Dynamic content can be handled with useState and useEffect. Here’s an example:

import { useState, useEffect, useCallback, useRef } from 'react';

const CodeEditor = ({ filename = "example.js", content, onSave }) => {
  const [value, setValue] = useState(content);
  const editorRef = useRef(null);

  useEffect(() => {
    if (editorRef.current?.view) {
      const currentValue = editorRef.current.view.state.doc.toString();
      if (currentValue !== content) {
        editorRef.current.view.dispatch({
          changes: { from: 0, to: currentValue.length, insert: content },
        });
        setValue(content);
      }
    }
  }, [content]);

  const handleChange = (val) => setValue(val);

  return (
    <CodeMirror
      ref={editorRef}
      value={value}
      extensions={[getLanguageExtension(filename)]}
      theme="dark"
      onChange={handleChange}
      height="600px"
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

This syncs content with the editor and allows for dynamic updates.

Step 4 - Saving Content with Keyboard Shortcuts

You can add a keyboard shortcut to save content using useEffect:

useEffect(() => {
  const handleKeyDown = (event) => {
    if (event.ctrlKey && event.key === 's') {
      event.preventDefault();
      onSave(filename, value);
    }
  };

  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, [filename, value, onSave]);
Enter fullscreen mode Exit fullscreen mode

This listens for Ctrl + S to save the editor content.

Full Component Code

Here’s the complete code for the dynamic CodeEditor component, based on my personal use case. Feel free to more of my code on GitHub.

'use client';

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import CopyButton from '@/components/snippets/CopyButton'
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { markdown } from '@codemirror/lang-markdown';
import { java } from '@codemirror/lang-java';
import { cpp } from '@codemirror/lang-cpp';
import { json } from '@codemirror/lang-json';
import { php } from '@codemirror/lang-php';
import { rust } from '@codemirror/lang-rust';
import { sql } from '@codemirror/lang-sql';
import { xml } from '@codemirror/lang-xml';
import { less } from '@codemirror/lang-less';
import { sass } from '@codemirror/lang-sass';
import { clojure } from '@nextjournal/lang-clojure';
import { csharp } from '@replit/codemirror-lang-csharp';
// Add more language imports as needed

// Import for ref
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';


const languageExtensions: { [key: string]: any } = {
    js: javascript({ jsx: true }),
    jsx: javascript({ jsx: true }),
    ts: javascript({ typescript: true }),
    tsx: javascript({ typescript: true, jsx: true }),
    py: python(),
    html: html(),
    css: css(),
    md: markdown(),
    java: java(),
    cpp: cpp(),
    json: json(),
    php: php(),
    rust: rust(),
    sql: sql(),
    xml: xml(),
    less: less(),
    sass: sass(),
    clojure: clojure(),
    csharp: csharp(),
    // Add more mappings for other languages
};

const getLanguageExtension = (filename: string) => {
    const extension = filename.split('.').pop();
    return languageExtensions[extension || ''] || javascript(); // Default to JavaScript if the language is not supported
};

const CodeEditor = ({
    filename = "example.js",
    content,
    onSave,
    onContentChange,
    editorRef
}: {
    filename?: string,
    content: string,
    onSave: (filename: string, content: string) => void,
    onContentChange?: (content: string) => void,
    editorRef: React.RefObject<ReactCodeMirrorRef>
}) => {
    const [value, setValue] = useState(content);

    useEffect(() => {
        // Check for editorRef.current.view before accessing it
        if (editorRef.current?.view) {
            const currentValue = editorRef.current.view.state.doc.toString();
            if (currentValue !== content) {
                console.log("Content Prop Change Detected, Updating Editor:", content);
                console.log("Current CodeMirror Doc State:", currentValue);

                setValue(content);
                editorRef.current.view.dispatch({
                    changes: { from: 0, to: currentValue.length, insert: content },
                });
            }
        }
    }, [content, editorRef]);

    const handleChange = useCallback((val: string) => {
        setValue(val);
        if (onContentChange) {
            onContentChange(val);
        }
    }, [onContentChange]);

    const handleKeyDown = useCallback((event: KeyboardEvent) => {
        if (event.ctrlKey && event.key === 's') {
            event.preventDefault(); // Prevent default save behavior
            onSave(filename, value); // Call the save function with current content
        }
    }, [filename, value, onSave]);

    useEffect(() => {
        window.addEventListener('keydown', handleKeyDown);
        return () => {
            window.removeEventListener('keydown', handleKeyDown);
        };
    }, [handleKeyDown]);

    const languageExtension = useMemo(() => getLanguageExtension(filename), [filename]);

    return (
        <div>
            <CodeMirror
                ref={editorRef}
                value={value}
                height="600px"
                extensions={[languageExtension]}
                theme="dark"
                onChange={handleChange}
                style={{
                    lineHeight: '1.6',
                }}
                className='max-w-full text-sm md:text-lg'
            />
            <div className='flex flex-row gap-4 items-center mt-4'>
                <Button onClick={() => onSave(filename, value)} className="mt-4">
                    Save
                </Button>
                <CopyButton editorRef={editorRef} className="mt-4" />
            </div>
        </div>
    );
};

export default CodeEditor;


Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial, you’ve learned how to set up a dynamic code editor with @uiw/react-codemirror, complete with language extensions, dynamic content handling, and keyboard shortcuts for saving. This setup provides a flexible and interactive editor ready to be integrated into your applications.

Let me know what topics you’d like me to cover next! Your feedback is valuable ♥

Happy Coding!

💖 💪 🙅 🚩
mrsupercraft
MrSuperCraft

Posted on October 30, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related