Dropdown value setting #1480
Replies: 9 comments 15 replies
-
A button will submit all component values in a page. You need to track the value of the dropdown to handle what function you'd like to run. eg: from h2o_wave import app, main, Q, ui, handle_on, on, copy_expando
@app("/")
async def serve(q: Q) -> None:
if q.args.dropdown is not None and q.args.dropdown != q.client.dropdown:
await dropdown(q)
elif not await handle_on(q):
await setup_page(q)
print(f"ARGS : {q.args}")
await q.page.save()
async def dropdown(q: Q):
print("dropdown is triggered")
copy_expando(q.args, q.client)
@on()
async def button(q: Q):
print("button is triggered")
async def setup_page(q: Q):
q.page['example'] = ui.form_card(box='1 1 2 2', items=[
ui.dropdown(name='dropdown', label='Dropdown', value="choice1", choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
ui.button("button", "button")
])
q.client.dropdown = 'choice1' |
Beta Was this translation helpful? Give feedback.
-
My 2 cents: In my opinion that's a major flaw regarding how the on-decorator and handle_on work. The latter invokes only the first matched handler and which one that will be depends on the order in which the components are defined. So the following would always print "button is triggered": async def setup_page(q: Q):
q.page['example'] = ui.form_card(box='1 1 2 2', items=[
ui.button("button", "button"),
ui.dropdown(name='dropdown', label='Dropdown', value="choice1", choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
]) That's super error prone if we don't restrict the use of "on" to buttons only (what @vopani's solution kinda implies). As soon as we use trigger=True somewhere is becomes even worse, because there is no way to identify the source of the submit that easily. @vopani's solution works, but personally I find that these required if-elif checks in serve undermine the usefulness of "on" (the purpose of it is to get rid of if-elif blocks in the first place). What I find useful is to exploit the fact, that there is always only 1 source of a submit:
For example, sorting button handlers to front of q.args would improve the handling and and allows to keep serve "clean". from h2o_wave import app, main, Q, ui, handle_on, on
button_click_handlers = set()
def button_handler(func):
button_click_handlers.add(func.__name__)
@wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
async def my_handle_on(q: Q):
"""put button_click_handlers to front of q.args"""
args_keys = set(q.args.__kv.keys())
args_keys = list(button_click_handlers & args_keys) + list(args_keys ^ button_click_handlers)
button_args_first = {k : q.args[k] for k in args_keys}
q.args.__kv.clear()
q.args.__kv.update(button_args_first)
return await handle_on(q)
@app("/")
async def serve(q: Q) -> None:
await my_handle_on(q)
await setup_page(q)
print(f"ARGS : {q.args}")
await q.page.save()
@button_handler
@on()
async def button(q: Q):
q.client.triggered = "button"
@on()
async def dropdown(q: Q):
q.client.triggered = f"drowdown: {q.args.dropdown}"
q.client.choice = q.args.dropdown
async def setup_page(q: Q):
q.page['example'] = ui.form_card(box='1 1 2 2', items=[
ui.dropdown(name='dropdown', trigger=True, label='Dropdown', value=q.client.choice or "choice1", choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
ui.button("button", "button"),
ui.text(q.client.triggered or "")
]) with @button_handler: without: |
Beta Was this translation helpful? Give feedback.
-
This is a very useful discussion - thanks for participating! Event-handling and callbacks almost always get gnarly, no matter what - age-old problem in computing :) This is why Wave didn't originally ship with a event/callback mechanism, and got added later because we were getting asked for a recommendation for organizing code (as opposed to a solution to a some other kind of problem). Therefore, The I suspect the main reason why this gets gnarly is that Personally, I wouldn't use I'm open to adding additional, alternate mechanisms to Wave, i.e. we could ship @Far0n's implementation with Wave, which can be imported as, say My main hesitation with changing the behavior of the current |
Beta Was this translation helpful? Give feedback.
-
This is not entirely correct. The order of handlers matter, the order of components don't. Observe: from h2o_wave import app, main, Q, ui, handle_on, on
@app("/")
async def serve(q: Q) -> None:
if not await handle_on(q):
await setup_page(q)
await q.page.save()
@on()
async def dropdown(q: Q):
show(q, [ui.text(f"dropdown triggered: {q.args}")])
@on()
async def button(q: Q):
show(q, [ui.text(f"button triggered: {q.args}")])
async def setup_page(q: Q):
show(q, [
ui.dropdown(name='dropdown', label='Dropdown', value="choice1", trigger=True, choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
ui.button("button", "button"),
])
def show(q, items):
q.page['example'] = ui.form_card(box='1 1 2 2', items=items) Clicking on the button produces:
Changing the dropdown produces:
Next, put the button before the dropdown: async def setup_page(q: Q):
show(q, [
ui.button("button", "button"),
ui.dropdown(name='dropdown', label='Dropdown', value="choice1", trigger=True, choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
]) Clicking on the button produces:
Changing the dropdown produces:
So, changing the order of components did not change the behavior. |
Beta Was this translation helpful? Give feedback.
-
Here's how I'd write the same example, without from h2o_wave import app, main, Q, ui, handle_on, on
@app("/")
async def serve(q: Q) -> None:
if not q.client.initialized:
await setup_page(q)
q.client.initialized = True
else:
q.client.choice = q.args.dropdown
show(q, [ui.text(f"You chose {q.client.choice}.")])
await q.page.save()
async def setup_page(q: Q):
q.client.choice = 'choice1'
show(q, [
ui.dropdown(name='dropdown', label='Dropdown', value=q.client.choice, trigger=True, choices=[
ui.choice(name='choice1', label='Choice 1'),
ui.choice(name='choice2', label='Choice 2'),
ui.choice(name='choice3', label='Choice 3'),
]),
ui.button("button", "button"),
])
def show(q, items):
q.page['example'] = ui.form_card(box='1 1 2 2', items=items To me, this is clearer and shorter, since any time |
Beta Was this translation helpful? Give feedback.
-
This was a useful discussion (and we found a subtle bug along the way). Here's a summary: What confused me was that To @ShehanIshanka's original question:
As explained here, That said, what caused the confusion was that you had two handlers defined, one for the dropdown and one for the button, and in Wave, you'll never run into the situation where either |
Beta Was this translation helpful? Give feedback.
-
I think that's on if the main sources of issues people running into with on. I believe it's quite natural to try something like:
So reacting to certain app state changes instead of copy_expando => process app state as holistic entity. |
Beta Was this translation helpful? Give feedback.
-
Thank you very much @vopani @Far0n and @lo5 for your responses. In my case, the code is organized with Based on the responses, I did something like this and it worked. But the problem is if you have several other components in your view, you need to handle them. @on()
async def dropdown(q: Q):
if q.args.dropdown is not None and q.args.dropdown != q.client.dropdown:
print("dropdown is triggered")
elif q.args.button:
await button(q) Also, I feel from #1484, this problem would be solved as the component which is lastly interacted, will be prioritized from it. |
Beta Was this translation helpful? Give feedback.
-
I actually believe this would be very useful: #1484 (comment) TLDR: Imagine having not just wave.emit events in Example q.events = {"toggle.triggered": True} # toggle was defined with trigger=True and has been clicked
q.args = {"toggle" : False} # new value of toggle => @on("toggle.triggered")
def toggle_handler(q: Q):
... I think that would work just fine, because afaik there would always at max 1 element in |
Beta Was this translation helpful? Give feedback.
-
When a value is set to the dropdown component, it would cause to trigger the dropdown. How can we bypass this?
As an example, in the following code if I click the button it won't execute code related to the button handling event. It executes the dropdown handling event code section.
Screen.Recording.2022-06-04.at.00.14.52.mov
Beta Was this translation helpful? Give feedback.
All reactions