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

Scoped event handlers #2510

Merged
merged 15 commits into from Mar 25, 2022
Merged

Scoped event handlers #2510

merged 15 commits into from Mar 25, 2022

Conversation

WorldSEnder
Copy link
Member

@WorldSEnder WorldSEnder commented Mar 11, 2022

Description

This addresses event handling in portals and scopes event handlers to the sub-tree controlled by each respective AppHandle, instead of installing them globally on document.body. Events also bubble through portals, i.e. up the virtual dom instead of the physical one. The benefit here is that events in iframes and other out-of-document content that don't natively capture/bubble there can be caught and handled.

Closes #2154
Closes #1564
Closes #2175

I think this is technically a breaking change, though I can't think of a reasonable (ab)use of the old, global, event handling style right now.

Still need to write test cases reproducing some issue of events in portals and checking they don't get handled twice, etc.
Need to maybe add a few test cases here and there. We'll see what comes up.

Checklist

  • I have run cargo make pr-flow
  • I have reviewed my own code
  • I have added tests (add more?)
  • Add documentation about bubbling behaviour

@WorldSEnder WorldSEnder added breaking change A-yew Area: The main yew crate S-waiting-on-author Status: awaiting action from the author of the issue/PR labels Mar 11, 2022
WorldSEnder added a commit to WorldSEnder/yew that referenced this pull request Mar 17, 2022
Async event dispatching is surprisingly complicated.
Make sure to see yewstack#2510 for details, comments and discussion
BundleRoot controls the element where listeners are registered.
Current impl always uses the global registry on document.body
we have create_root and create_ssr
WorldSEnder added a commit to WorldSEnder/yew that referenced this pull request Mar 17, 2022
Async event dispatching is surprisingly complicated.
Make sure to see yewstack#2510 for details, comments and discussion
Async event dispatching is surprisingly complicated.
Make sure to see yewstack#2510 for details, comments and discussion
@github-actions
Copy link

github-actions bot commented Mar 17, 2022

Visit the preview URL for this PR (updated for commit e416162):

https://yew-rs--pr2510-scoped-events-p27mqpv0.web.app

(expires Thu, 31 Mar 2022 17:31:34 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@WorldSEnder WorldSEnder marked this pull request as ready for review March 17, 2022 21:33
@WorldSEnder WorldSEnder removed the S-waiting-on-author Status: awaiting action from the author of the issue/PR label Mar 18, 2022
voidpumpkin
voidpumpkin previously approved these changes Mar 19, 2022
Copy link
Member

@voidpumpkin voidpumpkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of work done here. thx

@hamza1311
Copy link
Member

hamza1311 commented Mar 20, 2022

@WorldSEnder can you please resolve the merge conflicts? I'll review it after once this is merge-able.

  • Add documentation about bubbling behaviour

Does this also fix #2175?

@WorldSEnder WorldSEnder marked this pull request as draft March 20, 2022 14:57
this should take care of catching original events in shadow doms
github-actions[bot]
github-actions bot previously approved these changes Mar 22, 2022
@github-actions
Copy link

github-actions bot commented Mar 22, 2022

Size Comparison

examples master (KB) pull request (KB) diff
boids 309.131 311.400 +2.270
contexts 230.851 233.375 +2.524
counter 162.897 164.553 +1.655
dyn_create_destroy_apps 171.098 172.902 +1.805
file_upload 193.268 194.976 +1.708
function_memory_game 349.064 351.673 +2.608
function_router 22.254 22.254 0
function_todomvc 324.441 326.899 +2.458
futures 361.410 363.066 +1.656
game_of_life 205.580 207.214 +1.634
inner_html 154.726 156.414 +1.688
js_callback 170.219 171.902 +1.684
keyed_list 327.117 329.065 +1.948
mount_point 162.056 163.721 +1.665
nested_list 222.910 225.376 +2.466
node_refs 169.041 170.806 +1.765
password_strength 1849.893 1851.808 +1.915
portals 175.579 184.303 +8.724
router 584.637 588.363 +3.727
suspense 220.350 222.807 +2.457
timer 168.865 170.534 +1.669
todomvc 268.791 270.631 +1.840
two_apps 164.346 166.012 +1.666
webgl 169.007 170.818 +1.812

github-actions[bot]
github-actions bot previously approved these changes Mar 22, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 23, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 23, 2022
@WorldSEnder WorldSEnder marked this pull request as ready for review March 23, 2022 03:51
@WorldSEnder
Copy link
Member Author

@hamza1311 done, and yes I've added documentation addressing this

examples/portals/Cargo.toml Outdated Show resolved Hide resolved
state: self.state.clone(),
event: UpdateEvent::Shift(parent, next_sibling),
}))
let mut state_ref = self.state.borrow_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think shifting should be processed as a scheduler runnable.

For a couple of reasons:

  1. It guarantees that only 1 component is mutably borrowed at a time and the component is available to be borrowed.
  2. It allows higher priority events to be processed before it.
    (the component can destroy itself before a shift if both events present)
  3. It allows code that calls DOM APIs to be committed at smaller chunks by simply modifying the scheduler (future optimisation).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this for the simple reason that shifting is and should not be part of the lifecycle. It's also slightly easier not to have to capture the subtree.

It guarantees that only 1 component is mutably borrowed at a time and the component is available to be borrowed.

This should only be relevant if the component is nested in itself, which should not happen.

It allows higher priority events to be processed before it.

Given that other nodes are shifted immediately, delaying the shifting just messes up intermediate dom state. Think of a list of mixed components and normal elements that gets shifted. Delaying will (for some time) keep the components somewhere else than the elements.

Recipe for disaster, imo, and for event handling, finding the current correct parent needs to work on a correct hierarchy in the DOM. If the parent of a component is outdated, who knows what exactly happens, but it's just harder to remind everyone not to process events while components still have to be shifted. I don't see a benefit to delaying the shift. Again, a shift should only need to have to touch a handful, or even just one, node - all siblings at the top-level.

It allows code that calls DOM APIs to be committed at smaller chunks

I'm doubtful. Shifting an element should take a handful of calls, since it doesn't need to be done recursively, like rerendering. Once you drilled down to the root element, you just shift it and be done. The best candidate optimization I see is using the Range.extractContents API for lists, to move them in one chunk, but it doesn't work well with custom VRef(..) elements (the elements don't retain their identity, if I read the docs correctly. At least listeners are unregistered).

The real chunking should happen in view calls, which do work recursively and do require potentially a lot of calls. Just the amount of setAttribute calls and marshalling of strings accounts for ~30% of rendering costs in the benchmarks, if I remember numbers correctly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@futursolo I'll wait for your opinion on this, before I merge

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only be relevant if the component is nested in itself, which should not happen.

I think that mutably borrowing multiple components following the component tree downward is not a good idea.
As there're already other APIs relying on searching component states by immutably borrowing and searching the component tree upwards (e.g.: Scope::context()) and making sure them always not colliding seems a lot of work when working with APIs in this area.

Given that other nodes are shifted immediately, delaying the shifting just messes up intermediate dom state.

Rendering, attaching and destroying of components are all delayed as well.
Ultimately with #2396, every element will also be its own component which means every child element will be shifted at a later time as well.

Recipe for disaster, imo, and for event handling, finding the current correct parent needs to work on a correct hierarchy in the DOM.

Shifting into the detached parent of a Suspense should not be an issue as it's not possible to interact with elements inside the detached parent.

Should components use shifting when the Portal host changes?
IMHO, like it's not possible to change where a rendered application is mounted, I think Portal should be detached and re-attached if host changes. This should also allow to get rid of cache which simplifies the design.
(This is also how React handles Portal.)

Shifting an element should take a handful of calls,

There's nothing preventing an element from having 10,000 child elements.

Rendering is different than shifting, it will only result in the same number of API calls as the number of changes required for subsequent renders.
However, shifting 10,000 children will issue 10,000 DOM API calls every time shifting happens. If this can be split at component level, it means that we can split rendering into small chunks at component level (and element level with #2396) by simply modifying scheduler to commit a few runnables at a time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that mutably borrowing multiple components following the component tree downward is not a good idea.
As there're already other APIs relying on searching component states by immutably borrowing and searching the component tree upwards

Shifting doesn't use any of those APIs and I don't see a reason why that would change. There isn't and shouldn't be a way for user-code to run and do arbitrary things during shifting. That's the difference to "Rendering, attaching and destroying" which have user-defined code run. Shifts should be invisible, the user should only expect to control the tree downwards, etc. That's the argument of not being a lifecycle event.

Should components use shifting when the Portal host changes?

Maybe, but time for a future PR.

Shifting into the detached parent of a Suspense should not be an issue as it's not possible to interact with elements inside the detached parent.

Not directly, but events do run user-defined event handlers, which can run arbitrary code, so the DOM should be reasonably correct. I'm also not convinced that #2396 will actually switch to components under the hood (due to performance, creating a component has a noticeable overhead) and not just do the compile-time type checking. In any case, until that is decided, there'd be friction, should this PR be blocked until then? See below, 2396 is not relevant any time soon. So an event handler on an element, sibling to a component, could see inconsistent state if only the element is shifted and shifting of the component is delayed.

There's nothing preventing an element from having 10,000 child elements.

And most have a handful. Also, shifting 1e4 children would take the same time even if the lifecycle was delayed. You'd want to shift it all in one tick, lest be interrupted by other events and as said above, you shall not run user code in between. If there's truly a use-case for that many children, besides benchmarking, there are probably ways to correctly paginate and wrap those in a hierarchy (think of .git storing objects in folders labeling the first few characters of the object id, etc..) to limit the branching factor. And then you'd only have to shift the roots.

If the edge case works correctly, but has subpar performance, I don't think it's worth designing around it. Even more so if there's a reasonable work-around.

You'd want the shift of all those to take place in one tick, to prevent the browser from running pointless layout recomputations, anyway, as far as I am aware.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not convinced that #2396 will actually switch to components under the hood
(due to performance, creating a component has a noticeable overhead) and not just do the compile-time type checking.

We should assume that PR will not be merged, at least not in the current state. The current implementation has bigger problems than performance overhead (the trade-off between hundreds of if lets/huge arrays). Elements will need to be special cased at library level but with static typing.

In any case, until that is decided, there'd be friction, should this PR be blocked until then?

No, not at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until that is decided, there'd be friction

A disagreement in timing where components and elements are shifted out of order does not mean it creates issues. (Especially when the scheduler still works in a way that everything is will be run before yielding to the browser event loop which means an event listener cannot run before shifting finishes.)
Even if it does so, as long as Subtree is update at the same time as it's shifted, it would be fine?

Not directly, but events do run user-defined event handlers, which can run arbitrary code

Ultimately, this has something to do with the event handler behaviour.
(In React, Some events such as click will not be handled twice before a rendering is committed and thus preventing this issue. I am not sure how they implemented it though.)
See: facebook/react#20924 (comment)

You'd want the shift of all those to take place in one tick, to prevent the browser from running pointless layout recomputations, anyway, as far as I am aware.

You should keep the browser to be responsive whenever possible even if you are updating dom. React commits as much as 1 tick (in terms of refresh rate) worth of updates before letting browser to paint for this exact reason.

In addition, there's a difference between allowing user-defined logic to run and allowing browser to paint while updating dom. React uses a MessageChannel trick to make sure that as soon as the browser paints, it will be notified again to commit more changes.

If the edge case works correctly, but has subpar performance, I don't think it's worth designing around it.

If you want to keep this current design, I am fine to merge as-is and re-visit this at a future date when the situation changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked issue seems relevant, but as far as I can tell, we're already implementing the expected behavior (just skimmed it though).

If you want to keep this current design, I am fine to merge as-is and re-visit this at a future date when the situation changes.

Alright, I'll merge then, feel free to ping me if a related issue comes up. I'm not especially experienced with concurrent mode, so if that changes anything in the future, I'd like a notification 👍

packages/yew/src/dom_bundle/mod.rs Outdated Show resolved Hide resolved
examples/portals/src/main.rs Outdated Show resolved Hide resolved
packages/yew/src/dom_bundle/subtree_root.rs Outdated Show resolved Hide resolved
packages/yew/src/dom_bundle/subtree_root.rs Outdated Show resolved Hide resolved
github-actions[bot]
github-actions bot previously approved these changes Mar 24, 2022
@WorldSEnder WorldSEnder merged commit ee6a67e into yewstack:master Mar 25, 2022
@WorldSEnder WorldSEnder deleted the scoped-events branch March 25, 2022 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-yew Area: The main yew crate breaking change
Projects
None yet
4 participants