Dialog forms have inconsistent `q.args` behavior
- Dominant language
- Python
- Stars
- 4.3k
- Forks
- 371
- Avg merge
- 26d 1h
- Merged PRs (30d)
- 1
Description
#### Wave SDK Version, OS
Windows 10
Python 3.10.7
h2o-wave 1.0.0
#### Actual behavior
I'd like to use form verification: At least, check that no input is empty. If at least one input is empty, inform the user and keep all previous inputs. On every submit, all inputs are part of `q.args`. If the form is part of a card, this works fine. If the form is part of a dialog, it gets inconsistent:
- When submitting the form for the first time, all inputs, empty or not, are part of `q.args` and can be passed as values to the reloaded form.
- When submitting the form for the second time, NO inputs are part of `q.args` even not those that were visible before in the ui. The ui now also appears empty.
- Funnily, when submitting the form for a third time, ONLY the inputs that had a value initially are part of `q.args` but are now empty
- From the fourth time on, no inputs are part of `q.args` unless provided
Considering this it can get a extra funny:
- Submit the form with 1 value missing. The other values show up in the form.
- If you now add the missing value and submit again, the previously present values disappear and only the recent added input is maintained
- The game continues unless all inputs are filled in one go. Clicking on an input is not enough, it must be edited to be registered as a change
#### Expected behavior
I'd expect a dialog form to behave the same as a regular page form. But I am aware that it is not recommended to use dialogs for "content-heavy" workflows, so I'm also wondering if this behavior is "intended" or just a limitation of how the dialog box is implemented.
#### Steps To Reproduce
The app has two pages: `Page Form` and `Dialog Form`
On `Page Form`:
- Click `Send` without any input -> `q.args` includes all input fields
- Click `Send` without any input multiple times -> `q.args` includes all input fields
- Click `Send` with partial input -> `q.args` includes all input fields and the ui shows the previous input
- Click `Send` with partial input multiple times-> `q.args` includes all input fields and the ui shows the previous input
- Click `Send` with complete input -> `q.args` includes all input fields. The form is submitted and the fields are cleared
- Click `Reset` -> The fields are cleared
On `Dialog Form`:
- Repeat the same steps as for `Page Form`
- It will NOT show the same result. Instead, the "Actual behavior" will occur
Example code:
```python
from h2o_wave import main, app, Q, ui, on, run_on, data, core
from typing import Optional, List
# Use for page cards that should be removed when navigating away.
# For pages that should be always present on screen use q.page[key] = ...
def add_card(q, name, card) -> None:
q.client.cards.add(name)
q.page[name] = card
# Remove all the cards related to navigation.
def clear_cards(q, ignore: Optional[List[str]] = []) -> None:
if not q.client.cards:
return
for name in q.client.cards.copy():
if name not in ignore:
del q.page[name]
q.client.cards.remove(name)
#########################################################################
# PAGE FORM #############################################################
#########################################################################
def make_page1_items(q: Q, message: str = ""):
items = [
ui.textbox("page_input_1", label="Para 1", value=q.args.page_input_1 if q.args.page_input_1 else ""),
ui.textbox("page_input_2", label="Para 2", value=q.args.page_input_2 if q.args.page_input_2 else ""),
ui.textbox("page_input_3", label="Para 3", value=q.args.page_input_3 if q.args.page_input_3 else ""),
ui.buttons([
ui.button("page_send", label="Send", primary=True),
ui.button("page_reset", label="Reset", primary=False),
])
]
if message:
items.append(ui.text_m(f'{message}'))
return items
@on('#page1')
async def page1(q: Q):
q.page['sidebar'].value = '#page1'
clear_cards(q) # When routing, drop all the cards except of the main ones (header, sidebar, meta).
add_card(q, "page_form", ui.form_card(
box="horizontal",
items=make_page1_items(q)
))
@on('page_send')
async def page_send(q: Q):
print(q.args)
if all([q.args.page_input_1, q.args.page_input_2, q.args.page_input_3]):
q.args.page_input_1 = q.args.page_input_2 = q.args.page_input_3 = ""
add_card(q, "page_form", ui.form_card(
box="horizontal",
items=make_page1_items(q, message="Sent!")
))
else:
add_card(q, "page_form", ui.form_card(
box="horizontal",
items=make_page1_items(q, message="No field may be empty!")
))
@on('page_reset')
async def page_reset(q: Q):
print(q.args)
q.args.page_input_1 = q.args.page_input_2 = q.args.page_input_3 = ""
add_card(q, "page_form", ui.form_card(
box="horizontal",
items=make_page1_items(q, message="Reset!")
))
#########################################################################
# POPUP FORM ############################################################
#########################################################################
def make_popup_items(q: Q, message: str = ""):
items = [
ui.textbox("popup_input_1", label="Para 1", value=q.args.popup_input_1 if q.args.popup_input_1 else ""),
ui.textbox("popup_input_2", label="Para 2", value=q.args.popup_input_2 if q.args.popup_input_2 else ""),
ui.textbox("popup_input_3", label="Para 3", value=q.args.popup_input_3 if q.args.popup_input_3 else ""),
ui.buttons([
ui.button("popup_send", label="Send", primary=True),
ui.button("popup_reset", label="Reset", primary=False),
])
]
if message:
items.append(ui.text_m(f'{message}'))
return items
@on('#page2')
async def page2(q: Q):
q.page['sidebar'].value = '#page2'
clear_cards(q) # When routing, drop all the cards except of the main ones (header, sidebar, meta).
add_card(q, "popup_form", ui.form_card(
box="horizontal",
items=[ui.button("open_dialog", "Open Dialog")]
))
@on("open_dialog")
async def page2(q: Q):
q.page['meta'].dialog = ui.dialog(
title='Command',
name="popup",
closable=True,
events=['dismissed'],
items=make_popup_items(q)
)
@on(f'popup.dismissed')
async def commanding_popup_dismissed(q: Q):
q.page['meta'].dialog = None
@on('popup_send')
async def popup_send(q: Q):
print(q.args)
if all([q.args.popup_input_1, q.args.popup_input_2, q.args.popup_input_3]):
q.args.popup_input_1 = q.args.popup_input_2 = q.args.popup_input_3 = ""
q.page['meta'].dialog.items = make_popup_items(q, message="Sent")
else:
q.page['meta'].dialog.items = make_popup_items(q, message="No field may be empty!")
await q.page.save()
@on('popup_reset')
async def popup_reset(q: Q):
print(q.args)
q.args.popup_input_1 = q.args.popup_input_2 = q.args.popup_input_3 = ""
q.page['meta'].dialog.items = make_popup_items(q, message="Reset")
await q.page.save()
#########################################################################
#########################################################################
async def init(q: Q) -> None:
q.page['meta'] = ui.meta_card(box='', layouts=[ui.layout(breakpoint='xs', min_height='100vh', zones=[
ui.zone('main', size='1', direction=ui.ZoneDirection.ROW, zones=[
ui.zone('sidebar', size='250px'),
ui.zone('body', zones=[
ui.zone('content', zones=[
# Specify various zones and use the one that is currently needed. Empty zones are ignored.
ui.zone('horizontal', direction=ui.ZoneDirection.ROW),
ui.zone('vertical'),
ui.zone('grid', direction=ui.ZoneDirection.ROW, wrap='stretch', justify='center')
]),
]),
])
])])
q.page['sidebar'] = ui.nav_card(
box='sidebar', color='primary', title='My App', subtitle="Let's conquer the world!",
value=f'#{q.args["#"]}' if q.args['#'] else '#page1',
image='https://wave.h2o.ai/img/h2o-logo.svg', items=[
ui.nav_group('Menu', items=[
ui.nav_item(name='#page1', label='Page Form'),
ui.nav_item(name='#page2', label='Dialog Form'),
]),
],
secondary_items=[
ui.persona(title='John Doe', subtitle='Developer', size='s',
image='https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&h=750&w=1260'),
]
)
# If no active hash present, render page1.
if q.args['#'] is None:
await page1(q)
@app('/')
async def serve(q: Q):
# Run only once per client connection.
if not q.client.initialized:
q.client.cards = set()
await init(q)
q.client.initialized = True
# Handle routing.
await run_on(q)
await q.page.save()
```
Contributor guide
Research direction
Start with the provided `popup_send`, `popup_reset`, `make_popup_items`, and page-form handlers, then compare how dialog and regular form updates affect `q.args`. Reproduce the repeated-submit cases in the example app and trace the dialog implementation. Done means dialog forms preserve and report inputs consistently with page forms, or the limitation is documented if that behavior is intentional.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- full-stack
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100