-
-
Notifications
You must be signed in to change notification settings - Fork 324
Support raw JavaScript events #1289
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
Changes from 10 commits
13f588a
edf0498
8ae45ce
c11c2c4
db770ac
67cd1e7
36e33e4
61fc1d3
258c4de
5ce5c32
0b14bc8
23a7297
a109a3b
3a159af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,3 +221,36 @@ def outer_click_is_not_triggered(event): | |
await inner.click() | ||
|
||
await poll(lambda: clicked.current).until_is(True) | ||
|
||
|
||
async def test_javascript_event(display: DisplayFixture): | ||
@reactpy.component | ||
def App(): | ||
return reactpy.html.div( | ||
reactpy.html.div( | ||
reactpy.html.button( | ||
{ | ||
"id": "the-button", | ||
"onClick": """javascript: () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay with the general direction of this approach, but I don't like that things have to be prefixed with button_html = """<button onclick='console.log("hell world")'>Click Me</button>"""
my_vdom = html_to_vdom(button_html) If an indicator is absolutely required, then we can automatically transform any user-provided strings to prefix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to support that, but I couldn't come up with how. How would we differentiate an attribute from an event_handler? The I thought we could check if the attribute name starts with "on", but that falls short. Really the possibilities are endless when it comes to components. I could have some property called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe This logic could be used within There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use case that I'm specifically designing for has a property named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Off the top of my head, the best way of handling this would be a new data type. Perhaps it can be called class JavaScriptText(str):
... String content within an pattern = re.compile(r"^on[A-Z]")
if pattern.match(key) and isinstance(value, str):
value = JavaScriptText(value) ... but users should be allowed to manually construct them as needed. { "getRowId" : JavaScriptText("console.log('hello world')") } This would allow for a deterministic way of verifying if some string is def js_eval_compatible(value):
return isinstance(value, JavaScriptText) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love that - I will work to get it incorporated. |
||
let parent = document.getElementById("the-parent"); | ||
parent.appendChild(document.createElement("div")); | ||
}""", | ||
}, | ||
"Click Me", | ||
), | ||
reactpy.html.div({"id": "the-parent"}), | ||
) | ||
) | ||
|
||
await display.show(lambda: App()) | ||
|
||
button = await display.page.wait_for_selector("#the-button", state="attached") | ||
await button.click() | ||
await button.click() | ||
await button.click() | ||
parent = await display.page.wait_for_selector( | ||
"#the-parent", state="attached", timeout=0 | ||
) | ||
generated_divs = await parent.query_selector_all("div") | ||
|
||
assert len(generated_divs) == 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this statement would result in the
eval
being run instantly, rather than when the event occurs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I didn't realize it, apparently my first stab at this was with the only design that would actually work for this: using arrow function expressions.
The above assigns the arrow function to the
test
variable, which can then be executed.In what ways does this fall short? I need to think through that myself... I did just check that simply using a function name works:
Any concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still cannot come up with a case where you would need anything other than a direct function name or an arrow function expression. All
on<CamelCaseText>
event handlers are passed anevent
argument at a minimum, which is often relevant to the processing. For example:If we wanted to execute
eval(<javascript_text>)
at the very time the event is being fired, the above example would no longer work. If that were the case, how would I implement the above? Like this?Unfortunately, that would not work unless the user knows exactly what the "event" variable will be called in the scope in which it's called - which would have to be
e
for the example above to work. Sure, we could document that. But it seems more rigid and less appropriate.Thus, again, I'm pushing for either a function name or an arrow function expression.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than expecting
e.target.innerText
scenarios for plain text events...We really should be expecting something more akin to plain HTML inline JavaScript, where the appropriate context is provided via
this
(for example, `this.innerText)...In JavaScript, you can manipulate the value of
this
for aneval
statement by creating a simple wrapper.The end goal is that this piece of test code, which is valid HTML, should be able to work when used within ReactPy.
We can attach our own special properties onto
this
to give some more context to the event if needed. I could imagine that we would create athis.event
property that has all our typical event data into it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That all makes sense when looking at ReactPy as strictly a way to create valid HTML. However, I'm still stuck on my main use case - which is using ReactPy as a wrapper around an existing ReactJS component library: AG Grid. The way I've initially designed this fixes a bug I've had for months with a web page I'm building.
I have a grid with "Single Row Selection" enabled, which renders a checkbox with each row. I want to render a chart whenever a row is selected via clicking (i.e. "checking") the checkbox, so I also configure the "onRowSelected" event that pulls the selected row from the event and updates the ReactPy state with
set_show_chart(row_id)
. This state update causes the table to re-render the rows, which removes the check inside of the checkbox on the row that was selected.I dug forever wondering why the chart was being re-rendered. I found that the whole table wasn't being updated in the DOM, but only the rows. Then I found this section in their documentation about "Updating Row Data" and "Row IDs".
What I've implemented thus far in this PR finally fixed my bug by allowing me to implement this (shown with props as kwargs rather than a props dict):
I do recognize that
getRowId
is clearly not an event listener. So yeah, I'm grasping at a way to not only support standardized events, but also any Component property that expects a callable.Above you said:
Are you suggesting we could capture any/all arguments that are actually passed to the function and then attach them to our wrapped context and make them accessible via
this
? To generally support any callable, not just event-based, could we store the call args ontothis
with something likethis.args
?OR! I just found this via a google search: the arguments object. I didn't realize
arguments
is a reserved and standard context variable, much likethis
. So maybe youreval
solution would work here with something likegetRowId="String(arguments[0].data.id);
I'll go test that out.However, I will say that the main thing that bothers me about putting statements as the
JavaScriptText
rather than an actual function expression is that it feels less React-y... And somewhat breaks the intuitive nature for a developer (like me, at least). Could we not somehow support both? This comes down to where/how theeval
is actually called. Could we do this:Sorry for the big stream-of-consciousness... It helps me to document my thought process. Thanks for listening lol. I'll go play with these ideas and await any further from you, @Archmonger.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you suggested above, I'm completely ok with singleton functions (such as
JavaScriptText("(params) => String(params.data.id);")
) being provided to the web module prop as a callable.That's the most logical thing to do, especially given the fact that the
eval
statement would otherwise do absolutely nothing.