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.
H2O Wave has a built-in mechanism for editing a multiline text via its ui.textbox component, but it has significant downsides:
The app consists of 2 parts:
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:
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 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'
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()