Using Monaco editor as an input in a form

  • Gérald Barré

Monaco editor is the editor that powers VS Code. It is licensed under the MIT License and supports IE 11, Edge, Chrome, Firefox, Safari and Opera. This editor is very convenient to write markdown, json, and many other languages. It provides colorization, auto-complete, and lots of other features. You can extend he editor thanks to its API.

If you have a form where you want the user to enter markdown or code, it could be nice to use Monaco Editor instead of a textarea. In this post, we'll see how to integrate the editor like any other input element. This means the content automatically will be part of the POST request. The HTML I would like to write is the following:

<form method="post">

    <div class="form-group">
        <label for="Title">Title</label>
        <input id="Title" class="form-control" type="text" />
    </div>

    <div class="form-group">
        <label for="Content">Content</label>
        <!--
            👇 Custom element to display the Monaco Editor.
               The content of the editor is submitted with the form when you submit the form.
        -->
        <monaco-editor id="Content" language="json" name="sample" value="My content"></monaco-editor>
    </div>

    <button class="btn btn-primary">Submit</button>
</form>

#Integrating Monaco Editor into a web page

First, let's download Monaco Editor:

npm install --save monaco-editor

Now we can integrate the editor into a web page:

<form method="get" id="MyForm">

    <div class="form-group">
        <label for="Title">Title</label>
        <input id="Title" class="form-control" type="text" />
    </div>

    <div class="form-group">
        <label for="Content">Content</label>
        <div id="Content" style="min-height: 600px"></div>
    </div>

    <button class="btn btn-primary">Submit</button>
</form>

<script>
    var require = {
        paths: {
            'vs': '/node_modules/monaco-editor/min/vs',
        }
    };
</script>
<script src="node_modules/monaco-editor/min/vs/loader.js"></script>
<script>
    require(['vs/editor/editor.main'], () => {
        // Initialize the editor
        const editor = monaco.editor.create(document.getElementById("Content"), {
            theme: 'vs-dark',
            model: monaco.editor.createModel("# Sample markdown", "markdown"),
            wordWrap: 'on',
            automaticLayout: true,
            minimap: {
                enabled: false
            },
            scrollbar: {
                vertical: 'auto'
            }
        });
    });
</script>

#Submit the content of the editor like an input

When you submit a form, the browser will create an object of type FormData and add an entry with the value of every input, select and textarea that have a name attribute. You can add your own data by using the formdata event (documentation). This event provides a formdata object that you can feed with custom data. In this case we want to include the content of the Monaco Editor instance.

<script>
    require(['vs/editor/editor.main'], () => {
        // Initialize the editor
        const editor = monaco.editor.create(document.getElementById("Content"), {
            ...
        });

        const form = document.getElementById("MyForm");
        form.addEventListener("formdata", e =>
        {
            e.formData.append('content', editor.getModel().getValue());
        });
    });
</script>

Instead of doing that every time you want to integrate the editor in a form, you can create a custom element that wrap this logic.

#Wrap Monaco Editor into a custom element

One of the key features of the Web Components standard is the ability to create custom elements that encapsulate your functionality on an HTML page. Custom elements are well-supported:

Source: https://caniuse.com/#feat=custom-elementsv1

Let's create the custom element.

/// <reference path="../../node_modules/monaco-editor/monaco.d.ts" />

class MonacoEditor extends HTMLElement {
    // attributeChangedCallback will be called when the value of one of these attributes is changed in html
    static get observedAttributes() {
        return ['value', 'language'];
    }

    private editor: monaco.editor.IStandaloneCodeEditor | null = null;
    private _form: HTMLFormElement | null = null;

    constructor() {
        super();

        // keep reference to <form> for cleanup
        this._form = null;
        this._handleFormData = this._handleFormData.bind(this);
    }

    attributeChangedCallback(name: string, oldValue: any, newValue: any) {
        if (this.editor) {
            if (name === 'value') {
                this.editor.setValue(newValue);
            }

            if (name === 'language') {
                const currentModel = this.editor.getModel();
                if (currentModel) {
                    currentModel.dispose();
                }

                this.editor.setModel(monaco.editor.createModel(this._getEditorValue(), newValue));
            }
        }
    }

    connectedCallback() {
        this._form = this._findContainingForm();
        if (this._form) {
            this._form.addEventListener('formdata', this._handleFormData);
        }

        // editor
        const editor = document.createElement('div');
        editor.style.minHeight = '200px';
        editor.style.maxHeight = '100vh';
        editor.style.height = '100%';
        editor.style.width = '100%';
        editor.style.resize = 'vertical';
        editor.style.overflow = 'auto';

        this.appendChild(editor);

        // window.editor is accessible.
        var init = () => {
            require(['vs/editor/editor.main'], () => {
                console.log(monaco.languages.getLanguages().map(lang => lang.id));

                // Editor
                this.editor = monaco.editor.create(editor, {
                    theme: 'vs-dark',
                    model: monaco.editor.createModel(this.getAttribute("value"), this.getAttribute("language")),
                    wordWrap: 'on',
                    automaticLayout: true,
                    minimap: {
                        enabled: false
                    },
                    scrollbar: {
                        vertical: 'auto'
                    }
                });
            });

            window.removeEventListener("load", init);
        };

        window.addEventListener("load", init);
    }

    disconnectedCallback() {
        if (this._form) {
            this._form.removeEventListener('formdata', this._handleFormData);
            this._form = null;
        }
    }

    private _getEditorValue() {
        if (this.editor) {
            return this.editor.getModel().getValue();
        }

        return null;
    }

    private _handleFormData(ev: FormDataEvent) {
        ev.formData.append(this.getAttribute('name'), this._getEditorValue());
    }

    private _findContainingForm(): HTMLFormElement | null {
        // can only be in a form in the same "scope", ShadowRoot or Document
        const root = this.getRootNode();
        if (root instanceof Document || root instanceof Element) {
            const forms = Array.from(root.querySelectorAll('form'));
            // we can only be in one <form>, so the first one to contain us is the correct one
            return forms.find((form) => form.contains(this)) || null;
        }

        return null;
    }
}

customElements.define('monaco-editor', MonacoEditor);

interface FormDataEvent extends Event {
    readonly formData: FormData;
};

declare function require(files: string[], onLoaded: () => void): void;

You can know use the <monaco-editor> tag as in this example:

<form method="get">

    <div class="form-group">
        <label for="Title">Title</label>
        <input id="Title" class="form-control" type="text" />
    </div>

    <div class="form-group">
        <label for="Content">Content</label>
        <monaco-editor id="Content" language="json" name="sample"></monaco-editor>
    </div>

    <button class="btn btn-primary">Submit</button>
</form>


<script>
    var require = {
        paths: {
            'vs': '/node_modules/monaco-editor/min/vs',
        }
    };
</script>
<script src="node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="editor.js"></script>

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee