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

Behaviour of py-*: how to pass event #835

Closed
3 tasks done
dj-fiorex opened this issue Oct 6, 2022 · 25 comments
Closed
3 tasks done

Behaviour of py-*: how to pass event #835

dj-fiorex opened this issue Oct 6, 2022 · 25 comments
Labels
backlog issue has been triaged but has not been earmarked for any upcoming release classic Issues only applicable to PyScript versions 2023.05.1 and earlier ("PyScript Classic") tag: docs Related to the documentation

Comments

@dj-fiorex
Copy link
Contributor

Checklist

  • I added a descriptive title
  • I searched for other issues and couldn't find a solution or duplication
  • I already searched in Google and didn't find any good information or help

What happened?

Hello, i already used the py-* functionality to call a python function like this:

<input
       class="form-control"
       id="myfile"
       type="file"
       py-change="form_field_changed"
      />
...


 <py-script >
def form_field_changed(event):
</py-script>

Now this doesn't work anymore.
I noted that if i put brackets on function call like this form_field_changed() then the function is executed but comply that it require an argument (event)

is the Behaviour changed?
how can i access the event object?

Thanks

What browsers are you seeing the problem on? (if applicable)

Firefox, Chrome, Safari, Microsoft Edge, Other

Console info

No response

Additional Context

No response

@dj-fiorex dj-fiorex added needs-triage Issue needs triage type: bug Something isn't working labels Oct 6, 2022
@JeffersGlass
Copy link
Member

This behavior was changed as part of #686, which was included in the 2022.09.1 release of PyScript which was published last week. If you're linking to https://pyscript.net/latest/pyscript.js, then that's what probably broke your code.

The new syntax is:

<input
  py-change="form_field_changed()"
  ...
/>

<py-script>
    def form_field_changed():
        ...
</py-script>

This change was made to more closely mirror JavaScript's onevent syntax.

Note that PyScript's syntax does not currently make the event object available to the event handling method - you'll want to include logic for grabbing the data from the input in your form_field_changed function.

@JeffersGlass JeffersGlass added waiting on reporter Further information is requested from the reported and removed type: bug Something isn't working needs-triage Issue needs triage labels Oct 6, 2022
@dj-fiorex
Copy link
Contributor Author

ok thank you for the explanation!
Maybe the best thing to do is to have some examples that highlight the usage of py-* attributes. Where can i create it?

@tedpatrick
Copy link
Contributor

Yes, we need a way to get the event passed cleanly into the function. Looks like an area of work.

@dj-fiorex
Copy link
Contributor Author

I'm ready to give some help in my free time, where can i look to get an overview of the event handler?

@JeffersGlass
Copy link
Member

JeffersGlass commented Oct 6, 2022

Thanks @dj-fiorex! That would be swell. The logic for the py-*event handling is in pyscript.ts.

Perhaps writing up an explanation or reference-guide for the docs section would be the best place for that info to go, if you're game to write it up. @marimeireles may have thoughts as to the best home for that info.

For what it's worth, not every possible event is currently captured (though all the 'common' ones are) - #801 has some details.


For what it's worth, and slightly orthogonal to your question here, events are available if you add the event listener separately:

<!-- py-*event syntax doesn't current pass event nor "this"-->
<button id="one" py-click="say_hello()">No Event with Py-click</button>
<py-script>
  from js import console,

  def say_hello():
    console.log("HELLO!")
</py-script>

<!-- but pyodide.ffi.wrappers.add_event_listener or Element.addEventListener do-->
<button id="two">add_event_listener passes event</button>
<py-script>
  from js import console, document
  from pyodide.ffi.wrappers import add_event_listener

  def hello_args(*args):
    console.log(f"Hi! I got some args! {args}")

  add_event_listener(document.getElementById("two"), "click", say_goodbye)
</py-script>

As @tedpatrick, we should make it possible either way, but if you need that functionality for a project you're working on now, that's a workaround.

@tedpatrick
Copy link
Contributor

tedpatrick commented Oct 6, 2022

I am working on this at 2 levels:

  1. How do we subscribe py-{event} at startup
  2. How do we support adding a py-{event} attribute at runtime after PyScript has initialized (mutation observer)
Case 1: <button py-click="foo()">ClickMe</button>             SUPPORTED
Case 2: <button py-click="foo">ClickMe</button>               PROPOSED

In case 2, if the target function has 1 argument, it gets passed the event. If no arguments, no event.

This is an area of risk in terms of API design, I would rather do the simple thing now. We can always get more advanced in time.

@tedpatrick
Copy link
Contributor

You can also use a create_proxy

<html>
<head>
    <link rel="stylesheet" href="https://pyscript.net/unstable/pyscript.css" />
    <script defer src="https://pyscript.net/unstable/pyscript.js"></script>
</head>
<body>
    <button id="two">add_event_listener passes event</button>
<py-script>
  from pyodide.ffi import create_proxy

  def hello_args(*args):
      console.log(f"Hi! I got some args! {args}")

  Element('two').element.addEventListener("click", create_proxy(hello_args))
</py-script>
</body>
</html>

@JeffersGlass
Copy link
Member

JeffersGlass commented Oct 6, 2022

+1 to sometime having a solution that works when attributes are added after initialization - from the amount of times this has come up recently, I suspect PyScript will ultimately be one large mutation observer. And not in a bad way!

Case 1: <button py-click="foo()">ClickMe</button> SUPPORTED
Case 2: <button py-click="foo">ClickMe</button> PROPOSED

-1 to case 2: that was the sole behavior in 2022.06.1, and we specifically moved away from it with PR #686, because it's not how script onevent syntax works, and there was preference (from @fpliger et al) to at least not have PyScript do things that are surprisingly different from JavaScript in these areas.

If it's possible:

Case 3: <button py-click="foo(event)">ClickMe</button>       POSSIBLE??

would be closest, I think, though I have no idea how to implement it.

@tedpatrick
Copy link
Contributor

That syntax is what several leading JS frameworks do today, it isn't standard but it is the common syntax for assigning attributes function values by name. vue react svelte

Much is possible, the key is what interface is best for building apps from a developer's perspective. I personally would use this all the time. And what if it worked depending on the target method's arguments? I believe there is merit in supporting both.

<button py-click="foo">ClickMe</button>
<button py-click="foo()">ClickMe</button>

@marimeireles
Copy link
Member

Hey @dj-fiorex, my input for docs is that we may start introducing real content to the Reference part.
I guess it'd be nice to have a) an explanation on how to use the function, b) a list of the possible events, c) maybe @tedpatrick's example on how to use it through proxy is useful to have it there?
Thank you for offering to contribute 🎉 that's awesome :)

@marimeireles marimeireles added tag: docs Related to the documentation backlog issue has been triaged but has not been earmarked for any upcoming release and removed waiting on reporter Further information is requested from the reported labels Oct 11, 2022
@tedpatrick
Copy link
Contributor

And now for something completely different...

In exploring how to address the issues in py-event attributes, I took a detour and ended up somewhere interesting.

There are 4 core issues:

  1. We need to get the event object into python for event handlers
  2. It isn't clear how to expose a python function in JS
  3. We need to support adding/removing these handlers at runtime (py-event is wired up once at startup)
  4. We need an easy way to subscribe to events in python.

API (does not work without framework modifications)

<body style="padding:2em;">
    <button onclick="py.foo()">call foo on click</button><br><br>
    <button onclick="py.boo(event)">call boo on click w event</button><br><br>
    <button onclick="py.loo(event, 123)">call loo on click w event + 123</button><br><br>
    <button onclick="py.nothing(event)">call nothing on click w event</button><br><br>
    <button id="empty_button">empty_button with mousemove event</button><br><br>
    <py-script>
import js

@handler
def foo():
    js.console.log("foo called")

@handler
def boo(e):
    js.console.log("boo called with event")
    js.console.log( e )

@handler
def loo(*e):
    js.console.log("loo called with *event")
    js.console.log( e[0] )
    js.console.log( e[1] )

@handler(event="click")
def doc_click(*e):
    js.console.log("doc_click called with *event")
    js.console.log( e[0] )

@handler( event="mousemove", element=Element("empty_button").element )
def button_mousemove(*e):
    js.console.log("button_mousemove called with *event")
    js.console.log( e[0] )

    </py-script>
</body>

This is using the on[event] attribute syntax in JS but it adds in a global py object to allow writing event handlers like so :

  • onclick="py.foo()"
  • onclick="py.loo(event)"
  • onclick="py.goo(42,event,1,2,3)"

within all on[event] attributes, the event object exists within the call. So we can use event and write it directly into a call to a python handler. Since this argument interface is shared across js and python, it is easy to wire this in seamlessly.

As for exposing python methods into the py object, there is a @handler decorator that can simply add a function or wire up the event handler without using html/js at all.

Here we expose the button_mousemove handler to js and subscribe it to the mousemove event for a button element.

@handler( event="mousemove", element=Element("empty_button").element )
def button_mousemove(*e):
    js.console.log("button_mousemove called with *event")

Here is a working version for review. Open the JS console to see this working fully. Source is here.

The irony is in exploring py-event, there might be a cleaner, simpler way to manage events within a PyScript application.

Note, this is VERY EARLY and VERY POC. If we were to move forward along these lines, the naming and defaults would change but I think it feels right.

Feedback welcome.

@pmp-p
Copy link

pmp-p commented Oct 12, 2022

i like love the py.* proxy for event ( i already use that in pygame-script)

but i don't think event has its place in arguments it should be where it is expected to be ie : js.window.event <= it's a pitfall.

while inline onclick arguments should be fully and strictly in javascript space. That would allow to pass this properly instead of setting builtins.this before reaching python side.
pitfall too there should only be one argument (event)

for solely accessing event the method Element('two').element.addEventListener("click", create_proxy(hello_args)) described earlier is the way.

@antocuni
Copy link
Contributor

i like love the py.* proxy for event ( i already use that in pygame-script), but i don't think event has its place in arguments it should be where it is expected to be ie : js.window.event

According to Mozilla's docs window.event is deprecated, isn't it?

@pmp-p
Copy link

pmp-p commented Oct 13, 2022

According to Mozilla's docs window.event is deprecated, isn't it?

Sure but the window.event on python side is empty, so it's safe to use it for what it was/is used for : passing event for argument-less inline onclick=click()

doing onclick=click(event) would imply addressing window.event ... So if it vanishes suddenly after deprecation how one would get the event in old code ?

At least window.event is a single place in the proxy to maintain and to adapt to future ways.
it's also easy to make https://pygame-web.github.io/showroom/pyscript/unify.html

@antocuni
Copy link
Contributor

Sure but the window.event on python side is empty, so it's safe to use it for what it was/is used for : passing event for argument-less inline onclick=click()

I don't understand. Are you proposing that we should write to window.event?
If so, I don't think it's not safe at all. Even on the Python side, js.window is a reference to the real window object on the JS side, so if we try to write a .event attribute on it, I'm sure things will explode.

doing onclick=click(event) would imply addressing window.event ... So if it vanishes suddenly after deprecation how one would get the event in old code ?

I admit that I am having troubles to understand this sentence of the mozilla docs:

you should instead use the Event passed into the event handler function.

does it mean that you should not use the onclick=... HTML attribute but that you have to manually use addEventListener if you want to access the event?

@tedpatrick
Copy link
Contributor

tedpatrick commented Oct 13, 2022 via email

@pmp-p
Copy link

pmp-p commented Oct 13, 2022

If so, I don't think it's not safe at all. Even on the Python side, js.window is a reference to the real window object on the JS side, so if we try to write a .event attribute on it, I'm sure things will explode.

indeed it can have side effects and it's wasted cycle to send it back to js, why not just set "builtins.event" then only on python side ( i updated https://pygame-web.github.io/showroom/pyscript/unify.html accordingly ) ?

ps @antocuni: it's "black -x" formatted now, and maybe that's why i wanted json in pyconfig instead of toml ;)

@antocuni
Copy link
Contributor

indeed it can have side effects and it's wasted cycle to send it back to js, why not just set "builtins.event" then only on python side

well, relying on global state seems suboptimal and a certain source of bug. I would strongly prefer a solution in which we pass event as a function argument (even if on the JS side is a global -- it's not a good reason for continuing doing wrong things).

Example of things which can go wrong with a global/window/builtins event:

def on_click():
    assert event.type == "click" # guaranteed to work
    await fetch_something()
    assert event.type == "click" # might fail if another event handler was fired in the meantime

@JeffersGlass
Copy link
Member

JeffersGlass commented Oct 13, 2022

@tedpatrick This is a very slick implementation! I agree with your four goals - the only thing that I think is missing is that there's no way to pass Python variables/objects as arguments, true? Since all arguments in that scheme live in JS?

Can I suggest some alternate solutions for achieving the same goals? I've re-ordered them for narrative purposes.


We need an easy way to subscribe to events in python. We need to support adding/removing these handlers at runtime (py-event is wired up once at startup)

I personally think both of these are covered by Pyodide's add_event_listener()/ remove_event_listener(), which are a combination of create_proxy and JavaScript's addEventListener/removeEventListener. So, the style of your last code example would be:

import pyodide
def button_mousemove(*e):
    js.console.log("button_mousemove called with *event")
    js.console.log( e[0] )

pyodide.ffi.add_event_listener( Element("empty_button").element, event="mousemove", button_mousemove)

Though perhaps having this functionality as a decorator in PyScript might be appealing? If we want to dynamically add this behavior to elements that gain a "py-[event]" attribute, I think a MutationObserver might be the way.


We need to get the event object into python for event handlers

This is something I've been noodling on, and have a working prototype of. An example of code would be:

<button id="b" py-click="my_python_function(event)">Run my_python_function</button>

The key additions are:

#pyscript.py
from js import window
from contextvars import ContextVar

event = None

def load_event(js_event):
    global event #See below, this will be a ContextVar
    event = js_event
   
window.loadEvent = load_event #See below
//pyscript.ts
el.addEventListener(event, (event) => {
    event.preventDefault();
    (async(event) => {window.loadEvent(event); await runtime.run(handlerCode)})(event);

This is just the core of the idea - rather than a global event object, a ContextVar could be used to deal with the case of overlapping events. And there's surely a better way to create a JS function that assigning it to an attribute of window. Also a type-safe way.


It isn't clear how to expose a python function in JS

If PyScript were to export a reference to the Pyodide interpretter, Python functions would be accessible at something like PyScript.interpretter.globals['func_name'] (see Pyodide.globals).

This has been a request for awhile per that issue, and I think would solve that issue in the same way the Pyodide does.


The interesting part here is that these are actually independent of on[event] does not conflict with py-event or vice versa.

I was just thinking the same thing!

@dj-fiorex
Copy link
Contributor Author

Hello guys, i'm a web developer for 90% of my time and 10% python, and i can say that in the web space the event handler receive an event as an argument.
What @tedpatrick suggests is the way to go in my opinion

@pauleveritt
Copy link

Small note...I agree with @antocuni about the event argument being deprecated. My IDE is yelling at me when it is used. 😀 Yes it is still implemented. But if it's a bad practice, it's a bad practice.

@antocuni
Copy link
Contributor

antocuni commented Oct 17, 2022

Small note...I agree with @antocuni about the event argument being deprecated. My IDE is yelling at me when it is used. grinning Yes it is still implemented. But if it's a bad practice, it's a bad practice.

Just to be extra sure that we are talking about the same thing: you mean the event global variable such as in <button onclick="console.log(event)">, right?

@pauleveritt
Copy link

@antocuni Yes. I will generally wear the hat for "Good IDE and tooling integration" and this is one of those places.

@marimeireles
Copy link
Member

I guess this will be fixed once #1200 is merged

@JeffersGlass JeffersGlass added the classic Issues only applicable to PyScript versions 2023.05.1 and earlier ("PyScript Classic") label Sep 15, 2023
@WebReflection
Copy link
Contributor

I know in classic this discussion had reasons to exist but with current PyScript, thisis the contract:

Case 1: <button py-click="foo()">ClickMe</button>             NOT SUPPORTED
Case 2: <button py-click="foo">ClickMe</button>               WORKS

For the @when use case and explicit trigger we should have a different and well defined issue, imho, so I am closing this as it won't get fixed in classic and it's pointless to argue about this in current PyScript as we discussed this long time and the current state is not going to change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backlog issue has been triaged but has not been earmarked for any upcoming release classic Issues only applicable to PyScript versions 2023.05.1 and earlier ("PyScript Classic") tag: docs Related to the documentation
Projects
Status: Next
Development

Successfully merging a pull request may close this issue.

8 participants