Skip to content

Commit 5f89450

Browse files
committed
Reentrant event listeners (#3037)
* add test case for reentrent event listeners * use Fn to allow reentrent event listeners
1 parent 583f733 commit 5f89450

File tree

2 files changed

+111
-13
lines changed

2 files changed

+111
-13
lines changed

packages/yew/src/dom_bundle/btag/listeners.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,43 @@ mod tests {
749749
.unwrap()
750750
})
751751
}
752+
753+
#[test]
754+
fn reentrant_listener() {
755+
#[derive(PartialEq, Properties, Default)]
756+
struct Reetrant {
757+
secondary_target_ref: NodeRef,
758+
}
759+
impl Mixin for Reetrant {
760+
fn view<C>(ctx: &Context<C>, state: &State) -> Html
761+
where
762+
C: Component<Message = Message, Properties = MixinProps<Self>>,
763+
{
764+
let targetref = &ctx.props().wrapped.secondary_target_ref;
765+
let onclick = {
766+
let targetref = targetref.clone();
767+
ctx.link().callback(move |_| {
768+
// Note: `click` (and dispatchEvent for that matter) swallows errors thrown
769+
// from listeners and reports them as uncaught to the console. Hence, we
770+
// assert that we got to the second event listener instead, by dispatching a
771+
// second Message::Action
772+
click(&targetref);
773+
Message::Action
774+
})
775+
};
776+
let onclick2 = ctx.link().callback(move |_| Message::Action);
777+
html! {
778+
<div>
779+
<button {onclick} ref={&ctx.props().state_ref}>{state.action}</button>
780+
<a onclick={onclick2} ref={targetref}></a>
781+
</div>
782+
}
783+
}
784+
}
785+
let (_, el) = init::<Reetrant>();
786+
787+
assert_count(&el, 0);
788+
click(&el);
789+
assert_count(&el, 2);
790+
}
752791
}

packages/yew/src/dom_bundle/subtree_root.rs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
//! Per-subtree state of apps
22
3+
use std::borrow::Cow;
34
use std::cell::RefCell;
45
use std::collections::HashSet;
56
use std::hash::{Hash, Hasher};
67
use std::rc::{Rc, Weak};
78
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
89

9-
use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
10-
use wasm_bindgen::prelude::wasm_bindgen;
11-
use wasm_bindgen::JsCast;
12-
use web_sys::{Element, Event, EventTarget as HtmlEventTarget, ShadowRoot};
10+
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
11+
use wasm_bindgen::{JsCast, UnwrapThrowExt};
12+
use web_sys::{
13+
AddEventListenerOptions, Element, Event, EventTarget as HtmlEventTarget, ShadowRoot,
14+
};
1315

1416
use super::{test_log, Registry};
1517
use crate::virtual_dom::{Listener, ListenerKind};
@@ -113,6 +115,70 @@ impl From<&dyn Listener> for EventDescriptor {
113115
}
114116
}
115117

118+
// FIXME: this is a reproduction of gloo's EventListener to work around #2989
119+
// change back to gloo's implementation once it has been decided how to fix this upstream
120+
// The important part is that we use `Fn` instead of `FnMut` below!
121+
type EventClosure = Closure<dyn Fn(&Event)>;
122+
#[derive(Debug)]
123+
#[must_use = "event listener will never be called after being dropped"]
124+
struct EventListener {
125+
target: HtmlEventTarget,
126+
event_type: Cow<'static, str>,
127+
callback: Option<EventClosure>,
128+
}
129+
130+
impl Drop for EventListener {
131+
#[inline]
132+
fn drop(&mut self) {
133+
if let Some(ref callback) = self.callback {
134+
self.target
135+
.remove_event_listener_with_callback_and_bool(
136+
&self.event_type,
137+
callback.as_ref().unchecked_ref(),
138+
true, // Always capture
139+
)
140+
.unwrap_throw();
141+
}
142+
}
143+
}
144+
145+
impl EventListener {
146+
fn new(
147+
target: &HtmlEventTarget,
148+
desc: &EventDescriptor,
149+
callback: impl 'static + Fn(&Event),
150+
) -> Self {
151+
let event_type = desc.kind.type_name();
152+
153+
let callback = Closure::wrap(Box::new(callback) as Box<dyn Fn(&Event)>);
154+
// defaults: { once: false }
155+
let mut options = AddEventListenerOptions::new();
156+
options.capture(true).passive(desc.passive);
157+
158+
target
159+
.add_event_listener_with_callback_and_add_event_listener_options(
160+
&event_type,
161+
callback.as_ref().unchecked_ref(),
162+
&options,
163+
)
164+
.unwrap_throw();
165+
166+
EventListener {
167+
target: target.clone(),
168+
event_type,
169+
callback: Some(callback),
170+
}
171+
}
172+
173+
#[cfg(not(test))]
174+
fn forget(mut self) {
175+
if let Some(callback) = self.callback.take() {
176+
// Should always match, but no need to introduce a panic path here
177+
callback.forget();
178+
}
179+
}
180+
}
181+
116182
/// Ensures event handler registration.
117183
// Separate struct to DRY, while avoiding partial struct mutability.
118184
#[derive(Debug)]
@@ -135,15 +201,8 @@ impl HostHandlers {
135201
}
136202
}
137203

138-
fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + FnMut(&Event)) {
139-
let cl = {
140-
let desc = desc.clone();
141-
let options = EventListenerOptions {
142-
phase: EventListenerPhase::Capture,
143-
passive: desc.passive,
144-
};
145-
EventListener::new_with_options(&self.host, desc.kind.type_name(), options, callback)
146-
};
204+
fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + Fn(&Event)) {
205+
let cl = EventListener::new(&self.host, desc, callback);
147206

148207
// Never drop the closure as this event handler is static
149208
#[cfg(not(test))]

0 commit comments

Comments
 (0)