Skip to main content

Routing

App routing#

Your Wave app gets hosted at the route you passed to @app().

from h2o_wave import Q, main, app
@app('/foo')async def serve(q: Q):    pass

To host your app at localhost:10101/foo or www.example.com/foo, pass /foo to @app().

To host your app at localhost:10101 or www.example.com, pass / to @app(). Do this if you plan to host exactly one app and nothing else.

You can host multiple apps behind a single Wave server.

caution

/foo and /foo/bar are two distinct paths. /foo/bar is not interpreted as a sub-path of /foo.

Hash routing#

Wave apps support hash routing, a popular client-side mechanism where the location hash (the baz/qux in /foo/bar#baz/qux) can be used to decide which part of the UI to display.

Setting the location hash#

To set the location hash, prefix # to the name attribute of command-like components. When the command is invoked, the location hash is set to the name of the command.

For example, if a button is named foo is clicked, q.args.foo is set to True. Instead, if a button named #foo is clicked, the location hash is set to foo (q.args.foo is not set).

from h2o_wave import Q, main, app, ui
@app('/toss')async def serve(q: Q):    q.page['sides'] = ui.form_card(        box='1 1 4 4',        items=[            ui.button(name='#heads', label='Heads'),            ui.button(name='#tails', label='Tails'),        ],    )    await q.page.save()

Names don't have to be alphanumeric, so you can use names with nested sub-paths like #foo/bar, #foo/bar/baz, #foo/bar/baz/qux to make route-handling more manageable.

The components that support setting a location hash are:

  • ui.button()
  • ui.command()
  • ui.nav_item()
  • ui.tab()
  • ui.breadcrumb()

Getting the location hash#

To get the location hash, read q.args['#'] (a string). If the route in the browser's address bar is /foo/bar#baz/qux, q.args['#'] is set to baz/qux.

from h2o_wave import Q, main, app, ui
@app('/toss')async def serve(q: Q):    hash = q.args['#']    if hash == 'heads':        print('Heads!')    elif hash == 'tails':        print('Tails!')
    q.page.save()

Hash route switching#

Combining the two examples above gives us a basic pattern for handling routes and updating the user interface:

from h2o_wave import Q, main, app, ui
@app('/toss')async def serve(q: Q):    hash = q.args['#']
    if hash == 'heads':        q.page['sides'].items = [ui.message_bar(text='Heads!')]    elif hash == 'tails':        q.page['sides'].items = [ui.message_bar(text='Tails!')]    else:        q.page['sides'] = ui.form_card(            box='1 1 4 4',            items=[                ui.button(name='#heads', label='Heads'),                ui.button(name='#tails', label='Tails'),            ],        )
    await q.page.save()

Organizing code#

In most sizeable applications, the logic in the above if/elif/else conditionals can call into sub-functions, possibly spread across other modules:

from h2o_wave import Q, main, app, ui
async def on_heads(q: Q):    q.page['sides'].items = [ui.message_bar(text='Heads!')]
async def on_tails(q: Q):    q.page['sides'].items = [ui.message_bar(text='Tails!')]
async def setup_page(q: Q):    q.page['sides'] = ui.form_card(        box='1 1 4 4',        items=[            ui.button(name='#heads', label='Heads'),            ui.button(name='#tails', label='Tails'),        ],    )
@app('/toss')async def serve(q: Q):    hash = q.args['#']
    if hash == 'heads':        await on_heads(q)    elif hash == 'tails':        await on_tails(q)    else:        await setup_page(q)
    await q.page.save()

Reducing boilerplate#

As your application gets larger, using the above if/elif/else conditionals can seem tedious or repetitive. If so, you can use on and handle_on to reduce the boilerplate.

from h2o_wave import Q, main, app, ui, on, handle_on
@on('#heads')async def on_heads(q: Q):    q.page['sides'].items = [ui.message_bar(text='Heads!')]
@on('#tails')async def on_tails(q: Q):    q.page['sides'].items = [ui.message_bar(text='Tails!')]
async def setup_page(q: Q):    q.page['sides'] = ui.form_card(        box='1 1 4 4',        items=[            ui.button(name='#heads', label='Heads'),            ui.button(name='#tails', label='Tails'),        ],    )
@app('/toss')async def serve(q: Q):    if not await handle_on(q):        await setup_page(q)
    await q.page.save()

In the above example, the @on('#heads') is read as "if q.args['#'] is 'heads', then invoke the function the @on() is applied to" - in this case, on_heads().

Pattern matching#

The @on() annotation supports pattern matching.

This function is called when q.args['#'] == 'menu':

@on('#menu')async def show_menu(q: Q):    pass

This function is called when q.args['#'] == 'menu/donuts':

@on('#menu/donuts')async def show_donuts(q: Q):    pass

This function is called when q.args['#'] matches, say, 'menu/donuts/chocolate', with the parameter donut_name set to 'chocolate':

@on('#menu/donuts/{donut_name}')async def show_donut(q: Q, donut_name: str):    pass

Same as above, but donut_name is explicitly set to a string:

@on('#menu/donuts/{donut_name:str}')async def show_donut(q: Q, donut_name: str):    pass

This function is called when q.args['#'] matches, say, 'menu/donuts/42', with the parameter donut_id set to 42:

@on('#menu/donuts/{donut_id:int}')async def show_donut(q: Q, donut_id: int):    pass

This function is called when q.args['#'] matches, say, 'menu/donuts/7e21c93f-3a8f-4994-b63e-4275bc975e60', with the parameter donut_id set to the UUID:

@on('#menu/donuts/{donut_id:uuid}')async def show_donut(q: Q, donut_id: uuid.UUID):    pass

This function is called when q.args['#'] matches, say, 'menu/donuts/below/2.99', with the parameter donut_price set to 2.99:

@on('#menu/donuts/below/{donut_price:float}')async def show_donuts_below(q: Q, donut_price: float):    pass

Handling query arguments#

The @on() annotation can test the contents of q.args and invoke the corresponding handler.

This function is called when q.args.buy_now is found and the value is truthy:

@on('buy_now')async def buy_donuts(q: Q):    print(q.args.buy_now)

The handler can accept the value of the argument as well. Compare:

@on('buy_now')async def buy_donuts(q: Q, buy_now: bool):    print(buy_now)

This function is called when q.args.jam_filled is False:

@on('jam_filled', lambda x: x is False)async def buy_plain_donuts(q: Q):    pass

This function is called when q.args.jam_filled is True or False:

@on('jam_filled', lambda x: isinstance(x, bool)async def buy_donuts(q: Q, jam_filled: bool):    pass

This function is called when q.args.quantity between 42 and 420:

@on('quantity', lambda x: 42 <= x <= 420)async def buy_donuts(q: Q, quantity: bool):    pass

Handling events#

The @on() annotation can also test the contents of q.events and invoke the corresponding handler.

This function is called when q.events.donut_plot.select_marks is found and the value is truthy:

@on('donut_plot.select_marks')async def on_marks_selected(q: Q):    pass

This function is called when q.events.donut_plot.select_marks is 0:

@on('donut_plot.select_marks', lambda x: x == 0)async def on_marks_selected(q: Q):    pass

This function is called when q.events.donut_plot.select_marks is an integer:

@on('donut_plot.select_marks', lambda x: isinstance(x, int))async def on_marks_selected(q: Q, count: int):    pass

This function is called when q.events.donut_plot.select_marks between 42 and 420:

@on('donut_plot.select_marks', lambda x: 42 <= x <= 420)async def on_marks_selected(q: Q, count: int):    pass

Handling user logout#

To get notified when a user logs out of your apps, use the system-wide @system.logout event.

@on('@system.logout')async def on_user_logout(q: Q):    print(f'User {q.auth.username} logged out.')

Note that when a user logs out of the Wave daemon, all the apps linked to the daemon get notified with a @system.logout event.