Let’s have a look at how to provide our users with a truly amazing experience when we need to allow them to edit pieces of code or configuration.
We will use one of the most popular and well-known code editors called Monaco editor which powers VSCode. The resulting app will have the editor on the left side and a markdown card on the right to showcase how to actually capture the data and communicate with Wave.
Current state
H2O Wave has a built-in mechanism for editing a multiline text via its ui.textbox
component, but it has significant downsides:
- No syntax highlighting.
- No auto-completion.
- Regular font instead of monospaced which tends to be more readable.
- No find/replace functionality and other editor goodies.

General Approach
The app consists of 2 parts:
- Python part — regular Wave app.
- Javascript part — Monaco editor configuration.
The Javascript part
All the code is loaded using require.js, but you can consider it an implementation detail. The only Wave-related thing to note is events. We fire 2 kinds:
- Save event — when the user hits CMD or CTRL + S, used for updating our left-hand side markdown card.
- Change event — when the user types something, used to mark the editor card as dirty using
*
.
const baseUrl = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs' // UNIMPORTANT: RequireJS paths config for monaco editor. require.config({ paths: { 'vs': baseUrl, 'vs/language': `${baseUrl}/language`, } }) // UNIMPORTANT: Web worker registration for monaco editor. window.MonacoEnvironment = { getWorkerUrl: function (workerId, label) { return `data:text/javascript;charset=utf-8,${encodeURIComponent(` importScripts('${baseUrl}/base/worker/workerMain.js');` )}` } } // Load the editor javascript. require(['vs/editor/editor.main'], async () => { // Render the editor into our existing div with ID "editor". const editor = monaco.editor.create(document.getElementById('editor'), { language: 'python', }) // Use cmd (mac) or ctrl + S to save content. editor.addAction({ id: 'save-content', label: 'Save', keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], // Emit q.events.editor.save event on CMD+S. run: editor => window.wave.emit('editor', 'save', editor.getValue()) }) // When the editor text changes, emit q.events.editor.change event // to mark dirty state. editor.onDidChangeModelContent(() => window.wave.emit('editor', 'change', true)) })
In the javascript file above, we start with configuring our script paths, then load the editor (on line 17). Here we provide a div element (to be rendered by Wave) based on its unique id editor
and configure it to use syntax highlighting for python
. See this demo to explore all the available languages. If you want to tweak your editor a bit more, see all the available configuration options.
Once the editor has been created, we want to also setup up listeners from which we will emit Wave events using window.wave.emit
function. These are located within editor.addAction
and editor.onDidChangeModelContent
.
The Python part
The first thing we need to create is a single HTML div element that will serve as a container into which Monaco renders its magic. We can use ui.markup_card
as it’s designed for this exact purpose — rendering raw HTML. We also give the div a unique ID to easily select it in our JS file. Lastly, we want the editor to take up all the available card space so a few extra styles are necessary.
Then we just render a ui.markdown_card
to display saved text and demonstrate how can Wave and Monaco communicate.
from h2o_wave import main, app, Q, ui # UNIMPORTANT: Helper function to read the file. def read_file(p: str) -> str: with open(p, encoding='utf-8') as f: return f.read() @app('/') async def serve(q: Q): if not q.client.initialized: q.page['editor'] = ui.markup_card( box='1 1 6 10', title='Editor', # Render a container div and expand it to occupy whole card. content=''' <div id="editor" style="position:absolute;top:45px;bottom:15px;right:15px;left:15px"/> ''' ) q.page['text'] = ui.markdown_card(box='7 1 5 10', title='Text', content='') # Make sure to render the div prior to loading Javascript. await q.page.save() # Download the necessary javascript and render the Monaco editor. q.page['meta'] = ui.meta_card( box='', # Download external JS loader script. scripts=[ui.script(''' https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs/loader.min.js ''')], script=ui.inline_script( # Read our JS file and pass it as a string. content=read_file('app.js'), # Only run this script if "require" object is present # in browser's window object. requires=['require'], # Only run this script if our div container # with id "monaco-editor" is rendered. targets=['editor'] ), ) q.client.initialized = True await q.page.save()
Handling the events then is a piece of cake.
if q.events.editor: if q.events.editor.save: q.page['text'].content = q.events.editor.save q.page['editor'].title = 'Editor' elif q.events.editor.change: # Show "dirty" state by appending an * to editor card title. q.page['editor'].title = '*Editor'
The result
Using less than 100 total lines of code you get syntax highlighting and rich editing capabilities almost for free. Not bad at all I would say 🙂

from h2o_wave import main, app, Q, ui # UNIMPORTANT: Helper function to read the file. def read_file(p: str) -> str: with open(p, encoding='utf-8') as f: return f.read() @app('/') async def serve(q: Q): if not q.client.initialized: q.page['editor'] = ui.markup_card( box='1 1 6 10', title='Editor', # Render a container div and expand it to occupy whole card. content=''' <div id="editor" style="position:absolute;top:45px;bottom:15px;right:15px;left:15px"/> ''' ) q.page['text'] = ui.markdown_card(box='7 1 5 10', title='Text', content='') # Make sure to render the div prior to loading Javascript. await q.page.save() # Download the necessary javascript and render the actual Monaco editor. q.page['meta'] = ui.meta_card( box='', # Download external JS loader script. scripts=[ui.script(''' https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs/loader.min.js ''')], script=ui.inline_script( # Read our JS file and pass it as a string. content=read_file('app.js'), # Only run this script if "require" object is present # in browser's window object. requires=['require'], # Only run this script if our div container # with id "monaco-editor" is rendered. targets=['editor'] ), ) q.client.initialized = True if q.events.editor: if q.events.editor.save: q.page['text'].content = q.events.editor.save q.page['editor'].title = 'Editor' elif q.events.editor.change: # Show "dirty" state by appending an * to editor card title. q.page['editor'].title = '*Editor' await q.page.save()