DEV Community

Cover image for How to build a text editor app in typescript (svelte 5 + monaco-editor)
Pascal Lleonart
Pascal Lleonart

Posted on

1

How to build a text editor app in typescript (svelte 5 + monaco-editor)

In this post we'll create a small website that will permit the user to edit files with monaco-editor, select the file format and download it on their computer.

Features that we'll implement:

  • file editing
  • file name editing
  • language picker
  • download button

Project setup

First, let's install Svelte:

npx sv create myapp
Enter fullscreen mode Exit fullscreen mode

Make sure to select typescript.

Now move into the new folder and install monaco-editor:

pnpm install -D @monaco-editor/loader
Enter fullscreen mode Exit fullscreen mode

Coding time!

Adding the editor into the window

Let's create a MonacoEditor.svelte into our components folder:

<script lang="ts">
    import { type Monaco } from '@monaco-editor/loader'
    import { onDestroy, onMount } from 'svelte'

    let container: HTMLElement
    let monaco = $state<Monaco | undefined>()

    onMount(async () => {
        // we'll setup monaco here
    })

    onDestroy(() => {
        // we need to provide a way to delete our component, so our editor too
    })
</script>

<div id="editor-container" bind:this={container}></div>

<style>
    /* some styles */
    #editor-container {
        width: 100%;
        height: 600px;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

So we need to implement the functions that will setup our editor and delete it properly.

So let's create a file .src/lib/editor.ts:

import loader, { type Monaco } from '@monaco-editor/loader'


type SetupEditorOptions = {
    content?: string
    language?: string
}

export async function setupEditor(container: HTMLElement, options: SetupEditorOptions) {
}

export function deleteEditor(monaco: Monaco) {
}
Enter fullscreen mode Exit fullscreen mode

As you can see, to setup the editor, will need a HTML container and a default content and language.

For the removing system, we need to get all different editors of monaco and delete them. Here's the implementation:

export async function setupEditor(container: HTMLElement, options: SetupEditorOptions) {
    const monaco = await loader.init()

    monaco.editor.create(container, {
        value: options.content ?? "",
        language: options.language ?? "text"
    })

    return monaco
}


export function deleteEditor(monaco: Monaco) {
    const editors = monaco?.editor.getEditors()
    monaco?.editor.getModels().forEach((model) => model.dispose())
    editors?.forEach(editor => editor.dispose())
}
Enter fullscreen mode Exit fullscreen mode

In the MonacoEditor we have now:

<script lang="ts">
    import { type Monaco } from '@monaco-editor/loader'
    import { onDestroy, onMount } from 'svelte'
    import { deleteEditor, setupEditor } from '$lib/editor'

    let container: HTMLElement
    let monaco = $state<Monaco | undefined>()

    onMount(async () => {
        monaco = await setupEditor(container, {})
    })

    onDestroy(() => {
        if (monaco) deleteEditor(monaco)
    })
</script>
Enter fullscreen mode Exit fullscreen mode

Now we can put our MonacoEditor component into our ./src/pages/+page.svelte (our homepage):

<script>
    import MonacoEditor from "../components/MonacoEditor.svelte"
</script>

<MonacoEditor />
Enter fullscreen mode Exit fullscreen mode

And run, you can see that here's an editor in your window:

First look of our app: the monaco-editor is displayed


Change the editor's language

Now we want to have a <select> that will permit to the user to change the coloration of the text.

We create ./src/components/SelectLangForm.svelte:

<script lang="ts">
    let { monaco } = $props()

    let lang = $state("text")

    $effect(() => {
    })
</script>

<select bind:value={lang}>
    <option value={"javascript"}>js</option>
    <option value={"python"}>py</option>
    <option value={"text"}>txt</option>
    <!-- ... -->
</select>
Enter fullscreen mode Exit fullscreen mode

We want to implement a function that will update the language of the editor, so in our ./src/lib/editor.ts, we'll add this:

export function setLang(monaco: Monaco, language: string) {
    // we are getting the last editor to stock their text content and HTML container
    const lastEditor = monaco.editor.getEditors()[0]
    const lastEditorValue = lastEditor.getValue()

    deleteEditor(monaco)  // we don't need it anymore, so we delete him

    // we don't have any editor left, so let's create another
    monaco.editor.create(lastEditor.getContainerDomNode(), {
        value: lastEditorValue,
        language: language
    })

    return monaco
}
Enter fullscreen mode Exit fullscreen mode

Now our SelectLangForm looks like this:

<script lang="ts">
    import { setLang } from "$lib/editor"

    // ...

    $effect(() => {
        // if lang changes, change it in the editor too
        if (monaco) monaco = setLang(monaco, lang)
    })
</script>
Enter fullscreen mode Exit fullscreen mode

Now we need to call this component into MonacoEditor:

<script lang="ts">
    // ...

    import SelectLangForm from './SelectLangForm.svelte'

    // ....
</script>

<div class="banner">
    <SelectLangForm {monaco} />
</div>
Enter fullscreen mode Exit fullscreen mode

You can run now and test it!


Customize file name

Now we want to edit the file name.

So we need to create a new component called FileNameInput.svelte:

<script>
    let { fileName = $bindable() } = $props()
</script>

<input type="text" bind:value={fileName}>
Enter fullscreen mode Exit fullscreen mode

We need the fileName property bindable for reactivity.

Now let's integrate this new component into our main one:

<script lang="ts">
    // ...

    import SelectLangForm from './SelectLangForm.svelte'
    import FileNameInput from './FileNameInput.svelte'

    let container: HTMLElement
    let monaco = $state<Monaco | undefined>()
    let fileName = $state("untitled.txt")  // we declare our state here, with a default value

    //....
</script>

<div class="banner">
    <FileNameInput bind:fileName={fileName} />
    <SelectLangForm {monaco} />
</div>
Enter fullscreen mode Exit fullscreen mode

File download

Now we want that if the user clicks on a button, the current file is downloaded.

So let's create another component (the last one :)) DownloadFileButton.svelte:

<script lang="ts">
    function formatFile(content: string) {
        return `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`
    }

    function save() {
    }

    let { monaco = $bindable(), fileName = $bindable() } = $props()

    let href = $state(formatFile(""))
</script>

<svelte:window on:keydown={save} />

<a download={fileName} {href}>
    Download file
</a>
Enter fullscreen mode Exit fullscreen mode

Ok so when a key is pressed, the save() function will be called to get the editor's content and to put convert it into a kind of data link (the file content).

We need to implement the function that will get the editor's content, so in lib/editor.ts let's add this:

export function getEditorValue(monaco: Monaco) {
    return monaco.editor.getEditors()[0].getValue()
}
Enter fullscreen mode Exit fullscreen mode

Now our component looks like this:

<script lang="ts">
    import { getEditorValue } from "$lib/editor"

    function formatFile(content: string) {
        return `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`
    }

    function save() {
        href = formatFile(getEditorValue(monaco))
    }

    // ...
</script>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Now if we run we have something like this.

App render : an input, a select and a download button are added on top of the editor

As a homework, you'll need to bring new features to this app!

Bye, and thank you for reading this post!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Postgres on Neon - Get the Free Plan

No credit card required. The database you love, on a serverless platform designed to help you build faster.

Get Postgres on Neon

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay