Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"handle_on" depends on order of components in a form (it should not) #1484

Closed
lo5 opened this issue Jun 5, 2022 · 19 comments · Fixed by #2113
Closed

"handle_on" depends on order of components in a form (it should not) #1484

lo5 opened this issue Jun 5, 2022 · 19 comments · Fixed by #2113
Assignees
Labels
bug Bug in code py Related to Python Driver

Comments

@lo5
Copy link
Member

lo5 commented Jun 5, 2022

See discussion: #1480 (reply in thread)

Solution: Add a sort_args=False to handle_on. If set to True, sort q.args before iterating.

@lo5 lo5 added bug Bug in code py Related to Python Driver labels Jun 5, 2022
@lo5 lo5 self-assigned this Jun 5, 2022
@Far0n
Copy link

Far0n commented Jun 6, 2022

concerns about sort_args approach (for reference):

#1480 (reply in thread)
#1480 (reply in thread)

@Far0n
Copy link

Far0n commented Jun 6, 2022

Maybe it would be better for now to recommend to use on only for buttons. sort_args wont be needed then and it doesn't solve other tricky cases like trigger=True on toggles:

#1480 (reply in thread):
btw. toggles would still remain troublesome, because they have boolean values and a trigger event might give us toggle: False in q.args. Since that's not truthy handle_on wont invoke the handler for a True -> False switch (trigger = True).
So one actually needs to do @on("toggle", lambda x:True) to even have a chance to make it work.

Code_B9HpcaWUDu.mp4

The only thing that worked for me is to replace on with my own set of decorators, namely: on_click, on_change, on_event and on_path + corresponding handle_on replacements. From video above:

@app("/")
async def serve(q: Q) -> None:
    if q.client.toggle_01 is None:
        q.client.toggle_01 = False
    if q.client.toggle_02 is None:
        q.client.toggle_02 = False

    await handle_changes(q, compared_to=q.client)

    print(f"q.args : {q.args}")
    print(f"on_change.handlers: {on_change.handlers}")
    await setup_page(q)
    await q.page.save()

@on_change("toggle_{id}")
async def toggle_handler(q: Q, val, id):
    print(f"toggle_{id} changed, new val: {val}")
    q.client[f"toggle_{id}"] = val

async def setup_page(q: Q):
    q.page["example"] = ui.form_card(
        box="1 1 2 2",
        items=[
            ui.toggle(name="toggle_01", value=q.client.toggle_01, trigger=True),
            ui.toggle(name="toggle_02", value=q.client.toggle_02, trigger=True),
        ],
    )

@Far0n
Copy link

Far0n commented Jun 6, 2022

@lo5 wouldn't the following be an option: I believe currently q.events is only used for wave.emit() stuff. What if it would always hold the "source event" that led to invoking serve?

For example: Let's say we have an ui.toggle with trigger = True and we "click" it. Let's further assume the value changes from True to False. That would result in:

q.events = {"toggle.triggered": True}
q.args = {"toggle" : False}

That would enable a quite convenient usecase for @on in my opinion:

@on("toggle.triggered")
def toggle_handler(q: Q):
    ...

edit:
I also think button clicks should find their way into q.events, like so:

q.events = {"button.clicked": True}
q.args = {"button" : True}

For sake of backwards compatibility we can't remove it from q.args. So the redundancy could be an argument against it. But I feel one could still argue, that q.events holds the implicit "trigger = True" for the button and q.args it's value. So non-clicks could still be this, I guess:

q.events = {}
q.args = {"button" : False}

@lo5
Copy link
Member Author

lo5 commented Jun 7, 2022

@Far0n - I really like your suggestion. It's backwards-compatible, too. Seems obvious in hindsight :)

@vopani
Copy link
Contributor

vopani commented Jun 8, 2022

This might be the better solution we've been looking for.
@lo5 Can we try this out in a branch? I'd still want to test it on a bunch of written apps to confirm if it works well or not.

@Far0n
Copy link

Far0n commented Jun 8, 2022

Extra note:

I also think button clicks should find their way into q.events, like so:

q.events = {"button.clicked": True}
q.args = {"button" : True}

For sake of backwards compatibility we can't remove it from q.args. So the redundancy could be an argument against it. But I feel one could still argue, that q.events holds the implicit "trigger = True" for the button and q.args its value. So non-clicks could still be this, I guess:

q.events = {}
q.args = {"button" : False}

@padraic-shafer
Copy link

@Far0n : Could you share the code you used in handle_changes() and on_change(), etc.? I can guess at what's in there, but it would be even better to see what worked for you.

+1 for including these handlers in the main branch for dealing with triggered events...or the @on("toggle.triggered") that corresponds to q.events = {"toggle.triggered": True}.

@Far0n
Copy link

Far0n commented Dec 21, 2022

@pshafer-als our wave extensions are here: https://github.com/h2oai/model-validation/blob/master/mv/ui/wave/_routing_ex.py

@padraic-shafer
Copy link

@Far0n : Thanks! It looks like I don't have access to that repo. If ok, could you please add me to viewers?

@Far0n
Copy link

Far0n commented Dec 21, 2022

ahh sorry, its h2o internal only.

@Far0n
Copy link

Far0n commented Dec 21, 2022

I could create a little demo an put it into discussions. That's maybe a good place to check if others might find it helpful.

@padraic-shafer
Copy link

That would be great. Thank you!

@mturoci
Copy link
Collaborator

mturoci commented Aug 10, 2023

I like @Far0n 's suggestion and would like to expand on it.

I suggest deprecating the current handle_on in favor of a slightly semantically different routing version in the upcoming 1.0 release. The serve function is always triggered by a single event, which flushes the rest of the changes made and submits them to the server. The new mechanism would always fire a callback for the very last "event" that caused server submission. In practice, that can be a button click, trigger=True component, event or route change.

This way, the callbacks would fire "on changed" rather than "on found" wrt q.args.

The migration would be relatively painless: just import new_handle_on instead of handle_on and check if your app works - most of the apps should.

Needs discussion: @Far0n 's proposal suggest adding .clicked | .triggered info to give more info about the source. I am wondering what would be the usecase for it.

Personally, I wouldn't use @on/handle_on in my apps - it's a lot easier for me to reason about application state without them, because with @on/handle_on the application logic ends up being scattered across different event-handling functions, as opposed to being able to read all the logic in one place inside a single function. The single function might jump into other smaller functions, but the important thing is that I'm in control of the logic, not obscured behind handle_on(), and I can decide how to handle q.args based on the situation at hand :)

Disclaimer: I still fully agree with @lo5 that the best solution is the one that is tailored for the given use case with no extra framework magic to understand what's going on.

Will build the proposal in a separate branch for you folks to try out and get feedback.

@Far0n
Copy link

Far0n commented Aug 10, 2023

Needs discussion: @Far0n 's proposal suggest adding .clicked | .triggered info to give more info about the source. I am wondering what would be the usecase for it.

It gets a bit tricky currently to identify the root cause of a serve invokation if there are several components with trigger=True present (and maybe a submit button on top of it or some navigation link). Because everything lands in q.args one needs to implement extra logic to figure ot what happened to react accordingly. This is esp. tricky with the @on-handler, because only first match is invoked and it doesn't quite work for toggles atm without using lambda expression (i.e. True -> False toggle is not truthy, see #1484 (comment)). (and textbox if if becomes empty for that matter)

So the typical use case is to trigger the "@on" associated to the root cause instead of what is is found in q.args. But if I'm not mistaken your proposed solution would deal with that issue as well.

@mturoci
Copy link
Collaborator

mturoci commented Aug 10, 2023

So the typical use case is to trigger the "@on" associated to the root cause instead of what is is found in q.args. But if I'm not mistaken your proposed solution would deal with that issue as well.

Correct. This complexity will be hidden from regular wave app devs. I just asked to make sure you do not need to differentiate between .clicked and .triggered. But seems like the answer is no its whole purpose is to identify the serve invocation cause.

@Far0n
Copy link

Far0n commented Aug 10, 2023

But seems like the answer is no its whole purpose is to identify the serve invocation cause.

Yes, that's what I believe to remember at least :D (back when I thought about it .. so take it with a grain of salt ^^)

edit: so yeah I think these 2 things: a) root cause b) solution for the "is not truthy" issue with current (handle_on, @on). And a proper solution to a) should also solve b) I guess.

@Far0n
Copy link

Far0n commented Aug 10, 2023

@mturoci but if I think about it: wouldn't it be in general benefical of have the "root cause" accessible in q.events? So that we can make use of it in apps, that don't use [new]_handle_on?

@mturoci
Copy link
Collaborator

mturoci commented Aug 10, 2023

I currently expose the name of the cause in q.args['__wave_submission_name__']. It's prefixed with __ to mark it as "do not touch unless you know what you are doing". I suppose that should be enough?

@mturoci
Copy link
Collaborator

mturoci commented Aug 10, 2023

Looking for feedback in #2113 whoever is interested.

mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 7, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
mturoci added a commit that referenced this issue Sep 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Bug in code py Related to Python Driver
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants