DEV Community

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

Posted on

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!

Top comments (0)