Building a code editor within NextJS

ayseboogie

ayseboogie

Posted on January 15, 2022

Building a code editor within NextJS

I've found examples of building a code editor within React, but the npm packages that are used throw a window / navigator not found in NextJS. This is how I got around that.

Create App

First create your app
npx create-next-app@latest
then I am going to install Tailwind
run npm run dev and bam! Let's set up our folder structure. Create a components folder in the root directory and add 4 files: editor.js, editorApp.js, editor.module.css, and useLocalStorage.js.
Sweet, we are ready to build this editor.

editor.module.css

@media (min-width: 500px) {
    .topPane {
        display: flex;
    }
}
Enter fullscreen mode Exit fullscreen mode

Skeleton

Let's start by adding to our editor.js (you'll need to install the classnames module)

const Editor = () => {
    return(
        <div>Editor</div>
    )
}

export default Editor;
Enter fullscreen mode Exit fullscreen mode

Then we'll import that into our editorApp.js (3 times for html, css, and js)

import Editor from "./editor";
import * as style from "./editor.module.css";
import cn from "classnames";

const EditorApp = () => {
    return (
        <>
        <div className={cn("bg-zinc-500", style.topPane)}>
             <Editor />
             <Editor />
             <Editor />
         </div>
            <div style={{ height: "68vh" }}>
                <iframe
                title="output"
                sandbox="allow-scripts"
                frameBorder="0"
                height="100%"
                width="100%" />
            </div>
        </>
    )
};

export default EditorApp;
Enter fullscreen mode Exit fullscreen mode

And then we can rip out all the boiler code in index.js and import our editorApp

import CodeEditor from "../components/editorApp";

export default function Home() {
  return (
    <CodeEditor />
  )
}
Enter fullscreen mode Exit fullscreen mode

You should see Editor 3 times on your screen. Cool.

Editor.js

We need to install codemirror, so run npm install react-codemirror2-react-17 codemirror --save
Let's import the styles for codeMirror and our theme. You can test different themes here, and we will also import the codemirror editor.

import "codemirror/lib/codemirror.css";
import "codemirror/theme/tomorrow-night-bright.css";
import { Controlled as ControlledEditor } from "react-codemirror2-react-17";
Enter fullscreen mode Exit fullscreen mode

We have to import the languages differently as to not throw a ReferenceError of navigator is not defined.

let languageLoaded = false;
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
    require("codemirror/mode/xml/xml");
    require("codemirror/mode/css/css");
    require("codemirror/mode/javascript/javascript");
    languageLoaded = true;
}
Enter fullscreen mode Exit fullscreen mode

We are going to add create our editor now, with a title and body. This includes our props and handleChange function.

import "codemirror/lib/codemirror.css";
import "codemirror/theme/tomorrow-night-bright.css";
import { Controlled as ControlledEditor } from "react-codemirror2-react-17";

let languageLoaded = false;
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
    require("codemirror/mode/xml/xml");
    require("codemirror/mode/css/css");
    require("codemirror/mode/javascript/javascript");
    languageLoaded = true;
}

const Editor = (props) => {
    const { language, editorTitle, value, onChange, className } = props;

    function handleChange(editor, data, value) {
        onChange(value);
    }

    return(
        <div className={className}>
            <div className="flex justify-between text-white rounded-t-lg py-2 pr-2 pl-4 bg-zinc-700">
                {editorTitle}
            </div>
            <ControlledEditor
                onBeforeChange={handleChange}
                value={value}
                className="grow rounded-b-lg overflow-hidden text-left"
                options={{
                    lineWrapping: true,
                    lint: true,
                    mode: language,
                    theme: "tomorrow-night-bright",
                    lineNumbers: true,
                }}
            />
        </div>
    )
}

export default Editor;
Enter fullscreen mode Exit fullscreen mode

editorApp.js

We need to fill out our <Editor /> now. Add a
const [html, setHtml] = useState(''); (import useState)
and for the first editor

  <Editor
   language="xml"
   editorTitle="HTML"
   value={html}
   onChange={setHtml}
   />
Enter fullscreen mode Exit fullscreen mode

Copy this pattern for css and javascript.
Then we are going to add a useEffect to set the iframe srcDoc.

const [srcDoc, setSrcDoc] = useState("");

useEffect(() => {
    const timeout = setTimeout(() => {
      setSrcDoc(`
        <html lang="en">
          <body>${html}</body>
          <style>${css}</style>
          <script>${js}</script>
        </html>
      `);
    }, 200);

    return () => clearTimeout(timeout);
  }, [html, css, js]);
Enter fullscreen mode Exit fullscreen mode

and add the srcDoc prop as srcDoc to the iframe. You should have

import { useState, useEffect } from "react";
import Editor from "./editor";
import * as style from "./editor.module.css";
import cn from "classnames";

const EditorApp = () => {
  const [html, setHtml] = useState("");
  const [css, setCss] = useState("");
  const [js, setJs] = useState("");
  const [srcDoc, setSrcDoc] = useState("");

  useEffect(() => {
    const timeout = setTimeout(() => {
      setSrcDoc(`
        <html lang="en">
          <body>${html}</body>
          <style>${css}</style>
          <script>${js}</script>
        </html>
      `);
    }, 200);

    return () => clearTimeout(timeout);
  }, [html, css, js]);

  return (
    <>
      <div className={cn("bg-zinc-500", style.topPane)}>
        <Editor
          className="px-2 py-3 md:w-1/3 md:pl-3 md:pr-2"
          language="xml"
          editorTitle="HTML"
          value={html}
          onChange={setHtml}
        />
        <Editor
          className="px-2 py-3 md:w-1/3 md:px-2"
          language="css"
          editorTitle="CSS"
          value={css}
          onChange={setCss}
        />
        <Editor
          className="px-2 py-3 md:w-1/3 md:pl-1 md:pr-3"
          language="javascript"
          editorTitle="JS"
          value={js}
          onChange={setJs}
        />
      </div>
      <div style={{ height: "68vh" }}>
        <iframe
          srcDoc={srcDoc}
          title="output"
          sandbox="allow-scripts"
          frameBorder="0"
          height="100%"
          width="100%"
        />
      </div>
    </>
  );
};

export default EditorApp;

Enter fullscreen mode Exit fullscreen mode

You should now be able to type in the the editors and see your results displayed.

useLocalStorage.js

Last step.
In this component, we use localStorage, which will either throw a localStorage is not defined or window is not defined in NextJS.NextJS pre-renders every page which results in Static Site Generation or Server Side Rendering. localStorage is a property of the window object which is available on the client side / browser.
To get around this, we first have to check if the window is defined.

import { useEffect, useState } from "react";

function getStorageValue(key, initialValue) {
  if (typeof window !== "undefined") {
    const savedValue = localStorage.getItem(key);
    return savedValue !== null ? JSON.parse(savedValue) : initialValue;
  }
}

export const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    return getStorageValue(key, initialValue);
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};
Enter fullscreen mode Exit fullscreen mode

Import and replace useState in editorApp.js for your html, css, and js with useLocalStorage

  const [html, setHtml] = useLocalStorage("html", "");
  const [css, setCss] = useLocalStorage("css", "");
  const [js, setJs] = useLocalStorage("js", "");
Enter fullscreen mode Exit fullscreen mode

Boom

You should now have a working codeEditor. If you want to prefill any section with code, just put your code in the empty "" ex. const [html, setHtml] = useLocalStorage("html", "<div>foo</div>");. Keep in mind that localStorage is being saved so you may need to clear your browsing data one to many times.

nextCodeEditor

Getting Started

npm run dev
# or
yarn dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 with your browser to see the result.

This is my first post, I hope it was easy to follow and helps someone out :-)

💖 💪 🙅 🚩
ayseboogie
ayseboogie

Posted on January 15, 2022

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

Sign up to receive the latest update from our blog.

Related