mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Reentrant event listeners (#3037)
* add test case for reentrent event listeners * use Fn to allow reentrent event listeners
This commit is contained in:
parent
16df0150a9
commit
aebd225963
@ -749,4 +749,43 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reentrant_listener() {
|
||||||
|
#[derive(PartialEq, Properties, Default)]
|
||||||
|
struct Reetrant {
|
||||||
|
secondary_target_ref: NodeRef,
|
||||||
|
}
|
||||||
|
impl Mixin for Reetrant {
|
||||||
|
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||||
|
where
|
||||||
|
C: Component<Message = Message, Properties = MixinProps<Self>>,
|
||||||
|
{
|
||||||
|
let targetref = &ctx.props().wrapped.secondary_target_ref;
|
||||||
|
let onclick = {
|
||||||
|
let targetref = targetref.clone();
|
||||||
|
ctx.link().callback(move |_| {
|
||||||
|
// Note: `click` (and dispatchEvent for that matter) swallows errors thrown
|
||||||
|
// from listeners and reports them as uncaught to the console. Hence, we
|
||||||
|
// assert that we got to the second event listener instead, by dispatching a
|
||||||
|
// second Message::Action
|
||||||
|
click(&targetref);
|
||||||
|
Message::Action
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let onclick2 = ctx.link().callback(move |_| Message::Action);
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<button {onclick} ref={&ctx.props().state_ref}>{state.action}</button>
|
||||||
|
<a onclick={onclick2} ref={targetref}></a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (_, el) = init::<Reetrant>();
|
||||||
|
|
||||||
|
assert_count(&el, 0);
|
||||||
|
click(&el);
|
||||||
|
assert_count(&el, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
//! Per-subtree state of apps
|
//! Per-subtree state of apps
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::{Rc, Weak};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
|
|
||||||
use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
|
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||||
use wasm_bindgen::JsCast;
|
use web_sys::{
|
||||||
use web_sys::{Element, Event, EventTarget as HtmlEventTarget, ShadowRoot};
|
AddEventListenerOptions, Element, Event, EventTarget as HtmlEventTarget, ShadowRoot,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{test_log, Registry};
|
use super::{test_log, Registry};
|
||||||
use crate::virtual_dom::{Listener, ListenerKind};
|
use crate::virtual_dom::{Listener, ListenerKind};
|
||||||
@ -113,6 +115,70 @@ impl From<&dyn Listener> for EventDescriptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: this is a reproduction of gloo's EventListener to work around #2989
|
||||||
|
// change back to gloo's implementation once it has been decided how to fix this upstream
|
||||||
|
// The important part is that we use `Fn` instead of `FnMut` below!
|
||||||
|
type EventClosure = Closure<dyn Fn(&Event)>;
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[must_use = "event listener will never be called after being dropped"]
|
||||||
|
struct EventListener {
|
||||||
|
target: HtmlEventTarget,
|
||||||
|
event_type: Cow<'static, str>,
|
||||||
|
callback: Option<EventClosure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for EventListener {
|
||||||
|
#[inline]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(ref callback) = self.callback {
|
||||||
|
self.target
|
||||||
|
.remove_event_listener_with_callback_and_bool(
|
||||||
|
&self.event_type,
|
||||||
|
callback.as_ref().unchecked_ref(),
|
||||||
|
true, // Always capture
|
||||||
|
)
|
||||||
|
.unwrap_throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventListener {
|
||||||
|
fn new(
|
||||||
|
target: &HtmlEventTarget,
|
||||||
|
desc: &EventDescriptor,
|
||||||
|
callback: impl 'static + Fn(&Event),
|
||||||
|
) -> Self {
|
||||||
|
let event_type = desc.kind.type_name();
|
||||||
|
|
||||||
|
let callback = Closure::wrap(Box::new(callback) as Box<dyn Fn(&Event)>);
|
||||||
|
// defaults: { once: false }
|
||||||
|
let mut options = AddEventListenerOptions::new();
|
||||||
|
options.capture(true).passive(desc.passive);
|
||||||
|
|
||||||
|
target
|
||||||
|
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||||
|
&event_type,
|
||||||
|
callback.as_ref().unchecked_ref(),
|
||||||
|
&options,
|
||||||
|
)
|
||||||
|
.unwrap_throw();
|
||||||
|
|
||||||
|
EventListener {
|
||||||
|
target: target.clone(),
|
||||||
|
event_type,
|
||||||
|
callback: Some(callback),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn forget(mut self) {
|
||||||
|
if let Some(callback) = self.callback.take() {
|
||||||
|
// Should always match, but no need to introduce a panic path here
|
||||||
|
callback.forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensures event handler registration.
|
/// Ensures event handler registration.
|
||||||
// Separate struct to DRY, while avoiding partial struct mutability.
|
// Separate struct to DRY, while avoiding partial struct mutability.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -135,15 +201,8 @@ impl HostHandlers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + FnMut(&Event)) {
|
fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + Fn(&Event)) {
|
||||||
let cl = {
|
let cl = EventListener::new(&self.host, desc, callback);
|
||||||
let desc = desc.clone();
|
|
||||||
let options = EventListenerOptions {
|
|
||||||
phase: EventListenerPhase::Capture,
|
|
||||||
passive: desc.passive,
|
|
||||||
};
|
|
||||||
EventListener::new_with_options(&self.host, desc.kind.type_name(), options, callback)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Never drop the closure as this event handler is static
|
// Never drop the closure as this event handler is static
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user