Destiner's Notes

Using Custom Text Editor in Sandpack

6 February 2022

Sandpack React is bundled with the Codemirror editor. Sometimes, you want to use a different text editor. Here’s how to do it.

Code samples are written for Vue, although the general principle applies to other frameworks as well.

Sandpack Client

We will be using Sandpack Client. It’s a low-level library, so we will have full control over what editor we can use.

I’ve covered the basics of Sandpack Client in one of the previous articles. It is essentially a bare-bones sandboxing engine stripped out of any React-specific stuff.

First, install Sandpack Client:

npm i @codesandbox/sandpack-client

Create a new file with a Box component, with template containing an <iframe> element and Sandpack initialization inside an onMounted hook:

<template>
  <div class="sandbox">
    <iframe ref="iframeEl" />
  </div>
</template>

<script setup>
  import { SandpackClient } from '@codesandbox/sandpack-client';
  import { onMounted, ref } from 'vue';

  const iframeEl = ref(null);

  let client;

  onMounted(() => {
    client = new SandpackClient(iframeEl.value, {
      files: {
        '/index.html': {
          code: `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demo</title>
  </head>
  <body>
    <h1>Hello, Sandpack</h1>
  </body>
</html>
        `,
        },
      },
      dependencies: {},
      entry: '/index.html',
    });
  });
</script>

<style scoped>
  iframe {
    width: 500px;
    height: 300px;
    border: 1px solid #333;
  }
</style>

Basic editor

We will first add a basic <textarea> element to control the file source.

First, let’s move the source into a separate state variable so that we can update it later:

const code = ref(`...`);

onMounted(() => {
  client = new SandpackClient(iframeEl.value, {
    files: {
      '/index.html': {
        code: code.value,
      },
    },
    dependencies: {},
    entry: '/index.html',
  });
});

Then, add a <textarea> with the code model:

<textarea v-model="code" />

Finally, update Sandpack preview based on code change:

watch(code, (newCode) => {
  client.updatePreview({
    files: {
      '/index.html': {
        code: newCode,
      },
    },
    dependencies: {},
    entry: '/index.html',
  });
});

You can style the textarea input as well.

The final code would look like this:

<template>
  <div class="sandbox">
    <textarea v-model="code" />
    <iframe ref="iframeEl" />
  </div>
</template>

<script setup>
  import { SandpackClient } from '@codesandbox/sandpack-client';
  import { onMounted, ref, watch } from 'vue';

  const iframeEl = ref(null);
  const code = ref(`
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demo</title>
  </head>
  <body>
    <h1>Hello, Sandpack</h1>
  </body>
</html>`);

  let client;

  onMounted(() => {
    client = new SandpackClient(iframeEl.value, {
      files: {
        '/index.html': {
          code: code.value,
        },
      },
      dependencies: {},
      entry: '/index.html',
    });
  });

  watch(code, (newCode) => {
    client.updatePreview({
      files: {
        '/index.html': {
          code: newCode,
        },
      },
      dependencies: {},
      entry: '/index.html',
    });
  });
</script>

<style scoped>
  .sandbox {
    display: flex;
  }

  textarea {
    width: 500px;
    height: 300px;
    border: 1px solid #333;
  }

  iframe {
    width: 500px;
    height: 300px;
    border: 1px solid #333;
  }
</style>

Monaco editor

Now we will integrate Monaco into Sandpack.

Install the package:

npm i monaco-editor

Update the component template:

<template>
  <div class="sandbox">
    <div class="editor-wrapper" ref="monacoEl" />
    <iframe ref="iframeEl" />
  </div>
</template>

Import and initialize the editor:

import * as monaco from 'monaco-editor';

const monacoEl = ref(null);

onMounted(() => {
  const editor = monaco.editor.create(monacoEl.value, {
    value: code.value,
    language: 'javascript',
  });
  editor.onDidChangeModelContent(() => {
    const newCode = editor.getModel().getValue();
    code.value = newCode;
  });
  // ...
});

Update the styling:

.sandbox {
  display: flex;
}

.editor-wrapper {
  width: 500px;
  height: 300px;
  border: 1px solid #333;
}

iframe {
  width: 500px;
  height: 300px;
  border: 1px solid #333;
}

Here’s the full code:

<template>
  <div class="sandbox">
    <div class="editor-wrapper" ref="monacoEl" />
    <iframe ref="iframeEl" />
  </div>
</template>

<script setup>
  import { SandpackClient } from '@codesandbox/sandpack-client';
  import { onMounted, ref, watch } from 'vue';
  import * as monaco from 'monaco-editor';

  const monacoEl = ref(null);
  const iframeEl = ref(null);
  const code = ref(`
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demo</title>
  </head>
  <body>
    <h1>Hello, Sandpack</h1>
  </body>
</html>`);

  let client;

  onMounted(() => {
    const editor = monaco.editor.create(monacoEl.value, {
      value: code.value,
      language: 'javascript',
    });
    editor.onDidChangeModelContent(() => {
      const newCode = editor.getModel().getValue();
      code.value = newCode;
    });
    client = new SandpackClient(iframeEl.value, {
      files: {
        '/index.html': {
          code: code.value,
        },
      },
      dependencies: {},
      entry: '/index.html',
    });
  });

  watch(code, (newCode) => {
    client.updatePreview({
      files: {
        '/index.html': {
          code: newCode,
        },
      },
      dependencies: {},
      entry: '/index.html',
    });
  });
</script>

<style scoped>
  .sandbox {
    display: flex;
  }

  .editor-wrapper {
    width: 500px;
    height: 300px;
    border: 1px solid #333;
  }

  iframe {
    width: 500px;
    height: 300px;
    border: 1px solid #333;
  }
</style>

You should have a working integration by now. From there, you can customize the editor by changing the appearance, adding custom commands, and so on.