mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Add @when decorator (#1428)
* Add new _event_handling.py file with @when decorator * @when decorate is in pyscript package namespace/_all__ * Write tests in new test_event_handling.py * Add docs for @when decorator ------------ Co-authored-by: Mariana Meireles <marian.meireles@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
- Added the `@when` decorator for attaching Python functions as event handlers
|
||||||
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
|
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
docs/reference/API/when.md
Normal file
51
docs/reference/API/when.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# `@when`
|
||||||
|
|
||||||
|
`@when(event_type:str = None, selector:str = None)`
|
||||||
|
|
||||||
|
The `@when` decorator attaches the decorated function or Callable as an event handler for selected objects on the page. That is, when the named event is emitted by the selected DOM elements, the decorated Python function will be called.
|
||||||
|
|
||||||
|
If the decorated function takes a single (non-self) argument, it will be passed the [Event object](https://developer.mozilla.org/en-US/docs/Web/API/Event) corresponding to the triggered event. If the function takes no (non-self) argument, it will be called with no arguments.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
`event_type` - A string representing the event type to match against. This can be any of the [https://developer.mozilla.org/en-US/docs/Web/Events#event_listing](https://developer.mozilla.org/en-US/docs/Web/Events) that HTML elements may emit, as appropriate to their element type.
|
||||||
|
|
||||||
|
`selector` = A string containing one or more [CSS selectors](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors). The selected DOM elements will have the decorated function attacehed as an event handler.
|
||||||
|
|
||||||
|
## Examples:
|
||||||
|
|
||||||
|
The following example prints "Hello, world!" whenever the button is clicked. It demonstrates using the `@when` decorator on a Callable which takes no arguments:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button id="my_btn">Click Me to Say Hi</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#my_btn")
|
||||||
|
def say_hello():
|
||||||
|
print(f"Hello, world!")
|
||||||
|
</py-script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The following example includes three buttons - when any of the buttons is clicked, that button turns green, and the remaining two buttons turn red. This demonstrates using the `@when` decorator on a Callable which takes one argument, which is then passed the Event object from the associated event. When combined with the ability to look at other elements in on the page, this is quite a powerful feature.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="container">
|
||||||
|
<button>First</button>
|
||||||
|
<button>Second</button>
|
||||||
|
<button>Third</button>
|
||||||
|
</div>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
import js
|
||||||
|
|
||||||
|
@when("click", selector="#container button")
|
||||||
|
def highlight(evt):
|
||||||
|
#Set the clicked button's background to green
|
||||||
|
evt.target.style.backgroundColor = 'green'
|
||||||
|
|
||||||
|
#Set the background of all buttons to red
|
||||||
|
other_buttons = (button for button in js.document.querySelectorAll('button') if button != evt.target)
|
||||||
|
for button in other_buttons:
|
||||||
|
button.style.backgroundColor = 'red'
|
||||||
|
</py-script>
|
||||||
|
```
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from _pyscript_js import showWarning
|
from _pyscript_js import showWarning
|
||||||
|
|
||||||
|
from ._event_handling import when
|
||||||
from ._event_loop import LOOP as loop
|
from ._event_loop import LOOP as loop
|
||||||
from ._event_loop import run_until_complete
|
from ._event_loop import run_until_complete
|
||||||
from ._html import (
|
from ._html import (
|
||||||
@@ -51,4 +52,5 @@ __all__ = [
|
|||||||
"__version__",
|
"__version__",
|
||||||
"version_info",
|
"version_info",
|
||||||
"showWarning",
|
"showWarning",
|
||||||
|
"when",
|
||||||
]
|
]
|
||||||
|
|||||||
29
pyscriptjs/src/python/pyscript/_event_handling.py
Normal file
29
pyscriptjs/src/python/pyscript/_event_handling.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
import js
|
||||||
|
from pyodide.ffi.wrappers import add_event_listener
|
||||||
|
|
||||||
|
|
||||||
|
def when(event_type=None, selector=None):
|
||||||
|
"""
|
||||||
|
Decorates a function and passes py-* events to the decorated function
|
||||||
|
The events might or not be an argument of the decorated function
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
elements = js.document.querySelectorAll(selector)
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
# Function doesn't receive events
|
||||||
|
if not sig.parameters:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func()
|
||||||
|
|
||||||
|
for el in elements:
|
||||||
|
add_event_listener(el, event_type, wrapper)
|
||||||
|
else:
|
||||||
|
for el in elements:
|
||||||
|
add_event_listener(el, event_type, func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
@@ -316,6 +316,7 @@ class TestBasic(PyScriptTest):
|
|||||||
)
|
)
|
||||||
btn = self.page.wait_for_selector("button")
|
btn = self.page.wait_for_selector("button")
|
||||||
btn.click()
|
btn.click()
|
||||||
|
self.wait_for_console("hello world!")
|
||||||
assert self.console.log.lines[-1] == "hello world!"
|
assert self.console.log.lines[-1] == "hello world!"
|
||||||
assert self.console.error.lines == []
|
assert self.console.error.lines == []
|
||||||
|
|
||||||
|
|||||||
194
pyscriptjs/tests/integration/test_event_handling.py
Normal file
194
pyscriptjs/tests/integration/test_event_handling.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
from .support import PyScriptTest, skip_worker
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventHandler(PyScriptTest):
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_when_decorator_with_event(self):
|
||||||
|
"""When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object
|
||||||
|
"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
console_text = self.console.all.lines
|
||||||
|
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||||
|
assert "I've clicked [object HTMLButtonElement] with id foo_id" in console_text
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_when_decorator_without_event(self):
|
||||||
|
"""When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object
|
||||||
|
"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo():
|
||||||
|
print("The button was clicked")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
self.wait_for_console("The button was clicked")
|
||||||
|
assert "The button was clicked" in self.console.log.lines
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_multiple_when_decorators_with_event(self):
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<button id="bar_id">bar_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||||
|
@when("click", selector="#bar_id")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
console_text = self.console.all.lines
|
||||||
|
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||||
|
assert "I've clicked [object HTMLButtonElement] with id foo_id" in console_text
|
||||||
|
|
||||||
|
self.page.locator("text=bar_button").click()
|
||||||
|
console_text = self.console.all.lines
|
||||||
|
self.wait_for_console("I've clicked [object HTMLButtonElement] with id bar_id")
|
||||||
|
assert "I've clicked [object HTMLButtonElement] with id bar_id" in console_text
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_two_when_decorators(self):
|
||||||
|
"""When decorating a function twice, both should function"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<button class="bar_class">bar_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
@when("mouseover", selector=".bar_class")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"An event of type {evt.type} happened")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=bar_button").hover()
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
self.wait_for_console("An event of type click happened")
|
||||||
|
assert "An event of type mouseover happened" in self.console.log.lines
|
||||||
|
assert "An event of type click happened" in self.console.log.lines
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_two_when_decorators_same_element(self):
|
||||||
|
"""When decorating a function twice *on the same DOM element*, both should function"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
@when("mouseover", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"An event of type {evt.type} happened")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").hover()
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
self.wait_for_console("An event of type click happened")
|
||||||
|
assert "An event of type mouseover happened" in self.console.log.lines
|
||||||
|
assert "An event of type click happened" in self.console.log.lines
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_when_decorator_multiple_elements(self):
|
||||||
|
"""The @when decorator's selector should successfully select multiple
|
||||||
|
DOM elements
|
||||||
|
"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button class="bar_class">button1</button>
|
||||||
|
<button class="bar_class">button2</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector=".bar_class")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"{evt.target.innerText} was clicked")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=button1").click()
|
||||||
|
self.page.locator("text=button2").click()
|
||||||
|
self.wait_for_console("button2 was clicked")
|
||||||
|
assert "button1 was clicked" in self.console.log.lines
|
||||||
|
assert "button2 was clicked" in self.console.log.lines
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_when_decorator_duplicate_selectors(self):
|
||||||
|
""" """
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
console_text = self.console.all.lines
|
||||||
|
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||||
|
assert (
|
||||||
|
console_text.count("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||||
|
== 2
|
||||||
|
)
|
||||||
|
self.assert_no_banners()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_when_decorator_invalid_selector(self):
|
||||||
|
"""When the selector parameter of @when is invalid, it should show an error"""
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="foo_id">foo_button</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#.bad")
|
||||||
|
def foo(evt):
|
||||||
|
...
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.locator("text=foo_button").click()
|
||||||
|
msg = "Failed to execute 'querySelectorAll' on 'Document': '#.bad' is not a valid selector."
|
||||||
|
error = self.page.wait_for_selector(".py-error")
|
||||||
|
banner_text = error.inner_text()
|
||||||
|
|
||||||
|
if msg not in banner_text:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Expected message '{msg}' does not "
|
||||||
|
f"match banner text '{banner_text}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert any(msg in line for line in self.console.error.lines)
|
||||||
@@ -209,3 +209,63 @@ class TestDocsSnippets(PyScriptTest):
|
|||||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||||
|
|
||||||
assert "0\n1\n2\n" in py_terminal.inner_text()
|
assert "0\n1\n2\n" in py_terminal.inner_text()
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_reference_when_simple(self):
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<button id="my_btn">Click Me to Say Hi</button>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
@when("click", selector="#my_btn")
|
||||||
|
def say_hello():
|
||||||
|
print(f"Hello, world!")
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.page.get_by_text("Click Me to Say Hi").click()
|
||||||
|
self.wait_for_console("Hello, world!")
|
||||||
|
assert ("Hello, world!") in self.console.log.lines
|
||||||
|
|
||||||
|
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||||
|
def test_reference_when_complex(self):
|
||||||
|
self.pyscript_run(
|
||||||
|
"""
|
||||||
|
<div id="container">
|
||||||
|
<button>First</button>
|
||||||
|
<button>Second</button>
|
||||||
|
<button>Third</button>
|
||||||
|
</div>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import when
|
||||||
|
import js
|
||||||
|
|
||||||
|
@when("click", selector="#container button")
|
||||||
|
def highlight(evt):
|
||||||
|
#Set the clicked button's background to green
|
||||||
|
evt.target.style.backgroundColor = 'green'
|
||||||
|
|
||||||
|
#Set the background of all buttons to red
|
||||||
|
other_buttons = (button for button in js.document.querySelectorAll('button') if button != evt.target)
|
||||||
|
for button in other_buttons:
|
||||||
|
button.style.backgroundColor = 'red'
|
||||||
|
|
||||||
|
print("set") # Test Only
|
||||||
|
</py-script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def getBackgroundColor(locator):
|
||||||
|
return locator.evaluate(
|
||||||
|
"(element) => getComputedStyle(element).getPropertyValue('background-color')"
|
||||||
|
)
|
||||||
|
|
||||||
|
first_button = self.page.get_by_text("First")
|
||||||
|
assert getBackgroundColor(first_button) == "rgb(239, 239, 239)"
|
||||||
|
|
||||||
|
first_button.click()
|
||||||
|
self.wait_for_console("set")
|
||||||
|
|
||||||
|
assert getBackgroundColor(first_button) == "rgb(0, 128, 0)"
|
||||||
|
assert getBackgroundColor(self.page.get_by_text("Second")) == "rgb(255, 0, 0)"
|
||||||
|
assert getBackgroundColor(self.page.get_by_text("Third")) == "rgb(255, 0, 0)"
|
||||||
|
|||||||
Reference in New Issue
Block a user