diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs index 663f72797ce..e3d792a138d 100644 --- a/packages/yew-macro/src/html_tree/html_element.rs +++ b/packages/yew-macro/src/html_tree/html_element.rs @@ -269,7 +269,7 @@ impl ToTokens for HtmlElement { }; let listeners = if listeners.is_empty() { - quote! { ::std::vec![] } + quote! { ::yew::virtual_dom::listeners::Listeners::None } } else { let listeners_it = listeners.iter().map(|Prop { label, value, .. }| { let name = &label.name; @@ -278,7 +278,11 @@ impl ToTokens for HtmlElement { } }); - quote! { ::std::vec![#(#listeners_it),*].into_iter().flatten().collect() } + quote! { + ::yew::virtual_dom::listeners::Listeners::Pending( + ::std::boxed::Box::new([#(#listeners_it),*]) + ) + } }; // TODO: if none of the children have possibly None expressions or literals as keys, we can diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr index 4b1507959b8..6066546ee7e 100644 --- a/packages/yew-macro/tests/html_macro/element-fail.stderr +++ b/packages/yew-macro/tests/html_macro/element-fail.stderr @@ -302,69 +302,68 @@ error[E0277]: the trait bound `Option<{integer}>: IntoPropValue as IntoPropValue>>> = note: required by `into_prop_value` -error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `{integer}` +error[E0277]: the trait bound `{integer}: IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:51:28 | 51 | html! { }; - | ^ expected an `Fn<(MouseEvent,)>` closure, found `{integer}` + | ^ the trait `IntoPropValue>>` is not implemented for `{integer}` | ::: $WORKSPACE/packages/yew/src/html/listener/events.rs | - | / impl_action! { -3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event } -4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } -5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } + | / impl_short! { +133 | | onauxclick(MouseEvent) +134 | | onclick(MouseEvent) +135 | | ... | -102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } -103 | | } +196 | | ontransitionstart(TransitionEvent) +197 | | } | |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new` | - = help: the trait `Fn<(MouseEvent,)>` is not implemented for `{integer}` - = note: required because of the requirements on the impl of `IntoEventCallback` for `{integer}` + = help: the following implementations were found: + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + and 11 others -error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback` +error[E0277]: the trait bound `yew::Callback: IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:52:29 | 52 | html! { }; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | | - | expected an implementor of trait `IntoEventCallback` - | help: consider borrowing here: `&Callback::from(|a: String| ())` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue>>` is not implemented for `yew::Callback` | ::: $WORKSPACE/packages/yew/src/html/listener/events.rs | - | / impl_action! { -3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event } -4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } -5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } + | / impl_short! { +133 | | onauxclick(MouseEvent) +134 | | onclick(MouseEvent) +135 | | ... | -102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } -103 | | } +196 | | ontransitionstart(TransitionEvent) +197 | | } | |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new` - | - = note: the trait bound `yew::Callback: IntoEventCallback` is not satisfied - = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` -error[E0277]: the trait bound `Option<{integer}>: IntoEventCallback` is not satisfied +error[E0277]: the trait bound `Option<{integer}>: IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:53:29 | 53 | html! { }; - | ^^^^^^^ the trait `IntoEventCallback` is not implemented for `Option<{integer}>` + | ^^^^^^^ the trait `IntoPropValue>>` is not implemented for `Option<{integer}>` | ::: $WORKSPACE/packages/yew/src/html/listener/events.rs | - | / impl_action! { -3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event } -4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } -5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } + | / impl_short! { +133 | | onauxclick(MouseEvent) +134 | | onclick(MouseEvent) +135 | | ... | -102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } -103 | | } +196 | | ontransitionstart(TransitionEvent) +197 | | } | |_- required by this bound in `yew::html::onfocus::Wrapper::__macro_new` | = help: the following implementations were found: - as IntoEventCallback> - > as IntoEventCallback> + as IntoPropValue>>> + as IntoPropValue>> + as IntoPropValue>>> error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> $DIR/element-fail.rs:56:25 @@ -386,28 +385,22 @@ error[E0277]: the trait bound `Option: IntoPropValue as IntoPropValue>>> = note: required by `into_prop_value` -error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback` +error[E0277]: the trait bound `yew::Callback: IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:58:29 | 58 | html! { }; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | | - | expected an implementor of trait `IntoEventCallback` - | help: consider borrowing here: `&Callback::from(|a: String| ())` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue>>` is not implemented for `yew::Callback` | ::: $WORKSPACE/packages/yew/src/html/listener/events.rs | - | / impl_action! { -3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event } -4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } -5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } + | / impl_short! { +133 | | onauxclick(MouseEvent) +134 | | onclick(MouseEvent) +135 | | ... | -102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } -103 | | } +196 | | ontransitionstart(TransitionEvent) +197 | | } | |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new` - | - = note: the trait bound `yew::Callback: IntoEventCallback` is not satisfied - = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` error[E0277]: the trait bound `NotToString: IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:60:28 diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 484f4678535..a81c0d86edb 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -54,6 +54,7 @@ features = [ "Element", "ErrorEvent", "Event", + "EventInit", "EventTarget", "File", "FileList", @@ -66,6 +67,7 @@ features = [ "HtmlSelectElement", "HtmlTextAreaElement", "InputEvent", + "InputEventInit", "KeyboardEvent", "Location", "MessageEvent", diff --git a/packages/yew/src/callback.rs b/packages/yew/src/callback.rs index aa28cb59873..58fba1953a3 100644 --- a/packages/yew/src/callback.rs +++ b/packages/yew/src/callback.rs @@ -17,8 +17,18 @@ use std::rc::Rc; /// /// An `Rc` wrapper is used to make it cloneable. pub enum Callback { - /// A callback which can be called multiple times - Callback(Rc), + /// A callback which can be called multiple times with optional modifier flags + Callback { + /// A callback which can be called multiple times + cb: Rc, + + /// Setting `passive` to [Some] explicitly makes the event listener passive or not. + /// Yew sets sane defaults depending on the type of the listener. + /// See + /// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). + passive: Option, + }, + /// A callback which can only be called once. The callback will panic if it is /// called more than once. CallbackOnce(Rc>), @@ -28,14 +38,20 @@ type CallbackOnce = RefCell>>; impl From for Callback { fn from(func: F) -> Self { - Callback::Callback(Rc::new(func)) + Callback::Callback { + cb: Rc::new(func), + passive: None, + } } } impl Clone for Callback { fn clone(&self) -> Self { match self { - Callback::Callback(cb) => Callback::Callback(cb.clone()), + Callback::Callback { cb, passive } => Callback::Callback { + cb: cb.clone(), + passive: *passive, + }, Callback::CallbackOnce(cb) => Callback::CallbackOnce(cb.clone()), } } @@ -45,10 +61,16 @@ impl Clone for Callback { impl PartialEq for Callback { fn eq(&self, other: &Callback) -> bool { match (&self, &other) { - (Callback::Callback(cb), Callback::Callback(other_cb)) => Rc::ptr_eq(cb, other_cb), (Callback::CallbackOnce(cb), Callback::CallbackOnce(other_cb)) => { Rc::ptr_eq(cb, other_cb) } + ( + Callback::Callback { cb, passive }, + Callback::Callback { + cb: rhs_cb, + passive: rhs_passive, + }, + ) => Rc::ptr_eq(cb, rhs_cb) && passive == rhs_passive, _ => false, } } @@ -57,7 +79,7 @@ impl PartialEq for Callback { impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let data = match self { - Callback::Callback(_) => "Callback<_>", + Callback::Callback { .. } => "Callback<_>", Callback::CallbackOnce(_) => "CallbackOnce<_>", }; @@ -69,10 +91,10 @@ impl Callback { /// This method calls the callback's function. pub fn emit(&self, value: IN) { match self { - Callback::Callback(cb) => cb(value), + Callback::Callback { cb, .. } => cb(value), Callback::CallbackOnce(rc) => { let cb = rc.replace(None); - let f = cb.expect("callback in CallbackOnce has already been used"); + let f = cb.expect("callback contains `FnOnce` which has already been used"); f(value) } }; diff --git a/packages/yew/src/context.rs b/packages/yew/src/context.rs index d7596543f90..7572c82ea71 100644 --- a/packages/yew/src/context.rs +++ b/packages/yew/src/context.rs @@ -17,7 +17,7 @@ pub struct ContextProviderProps { /// The context provider component. /// /// Every child (direct or indirect) of this component may access the context value. -/// In order to consume contexts, [`ComponentLink::context`][Scope::context] method is used, +/// In order to consume contexts, [`Scope::context`][Scope::context] method is used, /// In function components the `use_context` hook is used. #[derive(Debug)] pub struct ContextProvider { diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 5215c0b5b74..c859f0cc28c 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -255,6 +255,29 @@ impl Scope { /// synchronously schedules a call to the [Component](Component) /// interface. pub fn callback(&self, function: F) -> Callback + where + M: Into, + F: Fn(IN) -> M + 'static, + { + self.callback_with_passive(None, function) + } + + /// Creates a `Callback` which will send a message to the linked + /// component's update method when invoked. + /// + /// Setting `passive` to [Some] explicitly makes the event listener passive or not. + /// Yew sets sane defaults depending on the type of the listener. + /// See + /// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). + /// + /// Please be aware that currently the result of this callback + /// synchronously schedules a call to the [Component](Component) + /// interface. + pub fn callback_with_passive( + &self, + passive: impl Into>, + function: F, + ) -> Callback where M: Into, F: Fn(IN) -> M + 'static, @@ -264,7 +287,10 @@ impl Scope { let output = function(input); scope.send_message(output); }; - closure.into() + Callback::Callback { + passive: passive.into(), + cb: Rc::new(closure), + } } /// Creates a `Callback` from an `FnOnce` which will send a message diff --git a/packages/yew/src/html/listener/events.rs b/packages/yew/src/html/listener/events.rs index 3789f3c6a37..516ad62bc2e 100644 --- a/packages/yew/src/html/listener/events.rs +++ b/packages/yew/src/html/listener/events.rs @@ -1,103 +1,216 @@ // Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events -impl_action! { - onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event } - onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } - oncancel(name: "cancel", event: Event) -> web_sys::Event => |_, event| { event } - oncanplay(name: "canplay", event: Event) -> web_sys::Event => |_, event| { event } - oncanplaythrough(name: "canplaythrough", event: Event) -> web_sys::Event => |_, event| { event } - onchange(name: "change", event: Event) -> web_sys::Event => |_, event| { event } - onclick(name: "click", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onclose(name: "close", event: Event) -> web_sys::Event => |_, event| { event } - oncontextmenu(name: "contextmenu", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - oncuechange(name: "cuechange", event: Event) -> web_sys::Event => |_, event| { event } - ondblclick(name: "dblclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - ondrag(name: "drag", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragend(name: "dragend", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragenter(name: "dragenter", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragexit(name: "dragexit", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragleave(name: "dragleave", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragover(name: "dragover", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondragstart(name: "dragstart", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondrop(name: "drop", event: DragEvent) -> web_sys::DragEvent => |_, event| { event } - ondurationchange(name: "durationchange", event: Event) -> web_sys::Event => |_, event| { event } - onemptied(name: "emptied", event: Event) -> web_sys::Event => |_, event| { event } - onended(name: "ended", event: Event) -> web_sys::Event => |_, event| { event } - onerror(name: "error", event: Event) -> web_sys::Event => |_, event| { event } - onfocus(name: "focus", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } - onfocusin(name: "focusin", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } - onfocusout(name: "focusout", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event } - // web_sys doesn't have a struct for `FormDataEvent` - onformdata(name: "formdata", event: Event) -> web_sys::Event => |_, event| { event } - oninput(name: "input", event: InputEvent) -> web_sys::InputEvent => |_, event| { event } - oninvalid(name: "invalid", event: Event) -> web_sys::Event => |_, event| { event } - onkeydown(name: "keydown", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event } - onkeypress(name: "keypress", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event } - onkeyup(name: "keyup", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event } - onload(name: "load", event: Event) -> web_sys::Event => |_, event| { event } - onloadeddata(name: "loadeddata", event: Event) -> web_sys::Event => |_, event| { event } - onloadedmetadata(name: "loadedmetadata", event: Event) -> web_sys::Event => |_, event| { event } - onloadstart(name: "loadstart", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event } - onmousedown(name: "mousedown", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmouseenter(name: "mouseenter", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmouseleave(name: "mouseleave", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmousemove(name: "mousemove", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmouseout(name: "mouseout", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmouseover(name: "mouseover", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onmouseup(name: "mouseup", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event } - onpause(name: "pause", event: Event) -> web_sys::Event => |_, event| { event } - onplay(name: "play", event: Event) -> web_sys::Event => |_, event| { event } - onplaying(name: "playing", event: Event) -> web_sys::Event => |_, event| { event } - onprogress(name: "progress", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event } - onratechange(name: "ratechange", event: Event) -> web_sys::Event => |_, event| { event } - onreset(name: "reset", event: Event) -> web_sys::Event => |_, event| { event } - onresize(name: "resize", event: Event) -> web_sys::Event => |_, event| { event } - onscroll(name: "scroll", event: Event) -> web_sys::Event => |_, event| { event } - onsecuritypolicyviolation(name: "securitypolicyviolation", event: Event) -> web_sys::Event => |_, event| { event } - onseeked(name: "seeked", event: Event) -> web_sys::Event => |_, event| { event } - onseeking(name: "seeking", event: Event) -> web_sys::Event => |_, event| { event } - onselect(name: "select", event: Event) -> web_sys::Event => |_, event| { event } - onslotchange(name: "slotchange", event: Event) -> web_sys::Event => |_, event| { event } - onstalled(name: "stalled", event: Event) -> web_sys::Event => |_, event| { event } - // web_sys doesn't have a struct for `SubmitEvent` - onsubmit(name: "submit", event: Event) -> web_sys::Event => |_, event| { event } - onsuspend(name: "suspend", event: Event) -> web_sys::Event => |_, event| { event } - ontimeupdate(name: "timeupdate", event: Event) -> web_sys::Event => |_, event| { event } - ontoggle(name: "toggle", event: Event) -> web_sys::Event => |_, event| { event } - onvolumechange(name: "volumechange", event: Event) -> web_sys::Event => |_, event| { event } - onwaiting(name: "waiting", event: Event) -> web_sys::Event => |_, event| { event } - onwheel(name: "wheel", event: WheelEvent) -> web_sys::WheelEvent => |_, event| { event } - - oncopy(name: "copy", event: Event) -> web_sys::Event => |_, event| { event } - oncut(name: "cut", event: Event) -> web_sys::Event => |_, event| { event } - onpaste(name: "paste", event: Event) -> web_sys::Event => |_, event| { event } - - onanimationcancel(name: "animationcancel", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event } - onanimationend(name: "animationend", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event } - onanimationiteration(name: "animationiteration", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event } - onanimationstart(name: "animationstart", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event } - ongotpointercapture(name: "gotpointercapture", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onloadend(name: "loadend", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event } - onlostpointercapture(name: "lostpointercapture", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointercancel(name: "pointercancel", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerdown(name: "pointerdown", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerenter(name: "pointerenter", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerleave(name: "pointerleave", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerlockchange(name: "pointerlockchange", event: Event) -> web_sys::Event => |_, event| { event } - onpointerlockerror(name: "pointerlockerror", event: Event) -> web_sys::Event => |_, event| { event } - onpointermove(name: "pointermove", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerout(name: "pointerout", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerover(name: "pointerover", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onpointerup(name: "pointerup", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event } - onselectionchange(name: "selectionchange", event: Event) -> web_sys::Event => |_, event| { event } - onselectstart(name: "selectstart", event: Event) -> web_sys::Event => |_, event| { event } - onshow(name: "show", event: Event) -> web_sys::Event => |_, event| { event } - ontouchcancel(name: "touchcancel", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event } - ontouchend(name: "touchend", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event } - ontouchmove(name: "touchmove", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event } - ontouchstart(name: "touchstart", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event } - ontransitioncancel(name: "transitioncancel", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } - ontransitionend(name: "transitionend", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } - ontransitionrun(name: "transitionrun", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } - ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event } + +macro_rules! impl_action { + ($($action:ident($type:ident) -> $ret:path => $convert:path)*) => {$( + impl_action!($action($type, false) -> $ret => $convert); + )*}; + ($($action:ident($type:ident, $passive:literal) -> $ret:path => $convert:path)*) => {$( + /// An abstract implementation of a listener. + #[doc(hidden)] + pub mod $action { + use crate::callback::Callback; + use crate::virtual_dom::{Listener, ListenerKind}; + use std::rc::Rc; + + /// A wrapper for a callback which attaches event listeners to elements. + #[derive(Clone, Debug)] + pub struct Wrapper { + callback: Callback, + } + + impl Wrapper { + /// Create a wrapper for an event-typed callback + pub fn new(callback: Callback) -> Self { + Wrapper { callback } + } + + #[doc(hidden)] + #[inline] + pub fn __macro_new( + callback: impl crate::html::IntoPropValue>>, + ) -> Option> { + let callback = callback.into_prop_value()?; + Some(Rc::new(Self::new(callback))) + } + } + + /// And event type which keeps the returned type. + pub type Event = $ret; + + impl Listener for Wrapper { + fn kind(&self) -> ListenerKind { + ListenerKind::$action + } + + fn handle(&self, event: web_sys::Event) { + self.callback.emit($convert(event)); + } + + fn passive(&self) -> bool { + match &self.callback { + Callback::Callback{passive, ..} => (*passive).unwrap_or($passive), + _ => $passive, + } + } + } + } + )*}; +} + +// Reduces repetition for common cases +macro_rules! impl_short { + ($($action:ident)*) => { + impl_action! { + $( + $action(Event) -> web_sys::Event => std::convert::identity + )* + } + }; + ($($action:ident($type:ident))*) => { + impl_action! { + $( + $action($type) -> web_sys::$type => crate::html::listener::cast_event + )* + } + }; +} + +// Unspecialized event type +impl_short! { + onabort + oncancel + oncanplay + oncanplaythrough + onclose + oncuechange + ondurationchange + onemptied + onended + onerror + onformdata // web_sys doesn't have a struct for `FormDataEvent` + oninvalid + + onload + onloadeddata + onloadedmetadata + + onpause + onplay + onplaying + + onratechange + onreset + onresize + onsecuritypolicyviolation + + onseeked + onseeking + + onselect + onslotchange + onstalled + onsuspend + ontimeupdate + ontoggle + onvolumechange + onwaiting + + onchange + + oncopy + oncut + onpaste + + onpointerlockchange + onpointerlockerror + onselectionchange + onselectstart + onshow +} + +// Specialized event type +impl_short! { + onauxclick(MouseEvent) + onclick(MouseEvent) + + oncontextmenu(MouseEvent) + ondblclick(MouseEvent) + + ondrag(DragEvent) + ondragend(DragEvent) + ondragenter(DragEvent) + ondragexit(DragEvent) + ondragleave(DragEvent) + ondragover(DragEvent) + ondragstart(DragEvent) + ondrop(DragEvent) + + onblur(FocusEvent) + onfocus(FocusEvent) + onfocusin(FocusEvent) + onfocusout(FocusEvent) + + onkeydown(KeyboardEvent) + onkeypress(KeyboardEvent) + onkeyup(KeyboardEvent) + + onloadstart(ProgressEvent) + onprogress(ProgressEvent) + onloadend(ProgressEvent) + + onmousedown(MouseEvent) + onmouseenter(MouseEvent) + onmouseleave(MouseEvent) + onmousemove(MouseEvent) + onmouseout(MouseEvent) + onmouseover(MouseEvent) + onmouseup(MouseEvent) + onwheel(WheelEvent) + + oninput(InputEvent) + + onsubmit(FocusEvent) + + onanimationcancel(AnimationEvent) + onanimationend(AnimationEvent) + onanimationiteration(AnimationEvent) + onanimationstart(AnimationEvent) + + ongotpointercapture(PointerEvent) + onlostpointercapture(PointerEvent) + onpointercancel(PointerEvent) + onpointerdown(PointerEvent) + onpointerenter(PointerEvent) + onpointerleave(PointerEvent) + onpointermove(PointerEvent) + onpointerout(PointerEvent) + onpointerover(PointerEvent) + onpointerup(PointerEvent) + + ontouchcancel(TouchEvent) + ontouchend(TouchEvent) + + ontransitioncancel(TransitionEvent) + ontransitionend(TransitionEvent) + ontransitionrun(TransitionEvent) + ontransitionstart(TransitionEvent) +} + +macro_rules! impl_passive { + ($($action:ident($type:ident))*) => { + impl_action! { + $( + $action($type, true) -> web_sys::$type + => crate::html::listener::cast_event + )* + } + }; +} + +// Best used with passive listeners for responsiveness +impl_passive! { + onscroll(Event) + + ontouchmove(TouchEvent) + ontouchstart(TouchEvent) } diff --git a/packages/yew/src/html/listener/mod.rs b/packages/yew/src/html/listener/mod.rs index 90297620367..6c64dc18c92 100644 --- a/packages/yew/src/html/listener/mod.rs +++ b/packages/yew/src/html/listener/mod.rs @@ -1,5 +1,4 @@ #[macro_use] -mod macros; mod events; use wasm_bindgen::JsCast; @@ -8,6 +7,19 @@ use web_sys::{Event, EventTarget}; use crate::Callback; pub use events::*; +/// Cast [Event] `e` into it's target `T`. +/// +/// This function mainly exists to provide type inference in the [impl_action] macro to the compiler +/// and avoid some verbosity by not having to type the signature over and over in closure +/// definitions. +#[inline] +pub(crate) fn cast_event(e: Event) -> T +where + T: JsCast, +{ + e.unchecked_into() +} + /// A trait to obtain a generic event target. /// /// The methods in this trait are convenient helpers that use the [`JsCast`] trait internally diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 12ede8a0c75..c413809dfe4 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -271,6 +271,8 @@ pub use web_sys; pub mod events { pub use crate::html::TargetCast; + pub use crate::virtual_dom::listeners::set_event_bubbling; + #[doc(no_inline)] pub use web_sys::{ AnimationEvent, DragEvent, ErrorEvent, Event, FocusEvent, InputEvent, KeyboardEvent, diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs new file mode 100644 index 00000000000..630606f6368 --- /dev/null +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -0,0 +1,834 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + ops::Deref, + rc::Rc, +}; +use wasm_bindgen::{prelude::*, JsCast}; +use web_sys::{Element, Event}; + +thread_local! { + /// Global event listener registry + static REGISTRY: RefCell = Default::default(); + + /// Key used to store listener id on element + static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); + + /// Cached reference to the document body + static BODY: web_sys::HtmlElement = crate::utils::document().body().unwrap(); +} + +/// Bubble events during delegation +static mut BUBBLE_EVENTS: bool = true; + +/// Set, if events should bubble up the DOM tree, calling any matching callbacks. +/// +/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event +/// handling performance. +/// +/// Note that yew uses event delegation and implements internal even bubbling for performance +/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event +/// handler has no effect. +/// +/// This function should be called before any component is mounted. +pub fn set_event_bubbling(bubble: bool) { + unsafe { + BUBBLE_EVENTS = bubble; + } +} + +/// The [Listener] trait is an universal implementation of an event listener +/// which is used to bind Rust-listener to JS-listener (DOM). +pub trait Listener { + /// Returns the name of the event + fn kind(&self) -> ListenerKind; + + /// Handles an event firing + fn handle(&self, event: web_sys::Event); + + /// Makes the event listener passive. See + /// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). + fn passive(&self) -> bool; +} + +impl std::fmt::Debug for dyn Listener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Listener {{ kind: {}, passive: {:?} }}", + self.kind().as_ref(), + self.passive(), + ) + } +} + +macro_rules! gen_listener_kinds { + ($($kind:ident)*) => { + /// Supported kinds of DOM event listeners + // Using instead of strings to optimise registry collection performance by simplifying + // hashmap hash calculation. + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] + #[allow(non_camel_case_types)] + #[allow(missing_docs)] + pub enum ListenerKind { + $( $kind, )* + } + + impl AsRef for ListenerKind { + fn as_ref(&self) -> &str { + match self { + $( Self::$kind => stringify!($kind), )* + } + } + } + }; +} + +gen_listener_kinds! { + onabort + onauxclick + onblur + oncancel + oncanplay + oncanplaythrough + onchange + onclick + onclose + oncontextmenu + oncuechange + ondblclick + ondrag + ondragend + ondragenter + ondragexit + ondragleave + ondragover + ondragstart + ondrop + ondurationchange + onemptied + onended + onerror + onfocus + onfocusin + onfocusout + onformdata + oninput + oninvalid + onkeydown + onkeypress + onkeyup + onload + onloadeddata + onloadedmetadata + onloadstart + onmousedown + onmouseenter + onmouseleave + onmousemove + onmouseout + onmouseover + onmouseup + onpause + onplay + onplaying + onprogress + onratechange + onreset + onresize + onscroll + onsecuritypolicyviolation + onseeked + onseeking + onselect + onslotchange + onstalled + onsubmit + onsuspend + ontimeupdate + ontoggle + onvolumechange + onwaiting + onwheel + oncopy + oncut + onpaste + onanimationcancel + onanimationend + onanimationiteration + onanimationstart + ongotpointercapture + onloadend + onlostpointercapture + onpointercancel + onpointerdown + onpointerenter + onpointerleave + onpointerlockchange + onpointerlockerror + onpointermove + onpointerout + onpointerover + onpointerup + onselectionchange + onselectstart + onshow + ontouchcancel + ontouchend + ontouchmove + ontouchstart + ontransitioncancel + ontransitionend + ontransitionrun + ontransitionstart +} + +/// A list of event listeners +#[derive(Debug)] +pub enum Listeners { + /// No listeners registered or pending. + /// Distinct from `Pending` with an empty slice to avoid an allocation. + None, + + /// Added to global registry by ID + Registered(u32), + + /// Not yet added to the element or registry + Pending(Box<[Option>]>), +} + +impl Listeners { + /// Register listeners and return their handle ID + fn register(el: &Element, pending: &[Option>]) -> Self { + Self::Registered(Registry::with(|reg| { + let id = reg.set_listener_id(el); + reg.register(id, pending); + id + })) + } + + /// Remove any registered event listeners from the global registry + pub(super) fn unregister(&self) { + if let Self::Registered(id) = self { + Registry::with(|r| r.unregister(id)); + } + } +} + +impl super::Apply for Listeners { + type Element = Element; + + fn apply(&mut self, el: &Self::Element) { + if let Self::Pending(pending) = self { + *self = Self::register(el, pending); + } + } + + fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + use Listeners::*; + + match (std::mem::take(self), ancestor) { + (Pending(pending), Registered(id)) => { + // Reuse the ID + Registry::with(|reg| reg.patch(&id, &*pending)); + *self = Registered(id); + } + (Pending(pending), None) => { + *self = Self::register(el, &pending); + } + (None, Registered(id)) => { + Registry::with(|reg| reg.unregister(&id)); + } + _ => (), + }; + } +} + +impl PartialEq for Listeners { + fn eq(&self, rhs: &Self) -> bool { + use Listeners::*; + + match (self, rhs) { + (None, None) => true, + (Registered(lhs), Registered(rhs)) => lhs == rhs, + (Registered(registered_id), Pending(pending)) + | (Pending(pending), Registered(registered_id)) => { + use std::option::Option::None; + + Registry::with(|reg| match reg.by_id.get(registered_id) { + Some(reg) => { + if reg.len() != pending.len() { + return false; + } + + pending.iter().filter_map(|l| l.as_ref()).all(|l| { + match reg.get(&EventDescriptor::from(l.deref())) { + Some(reg) => reg.iter().any(|reg| { + #[allow(clippy::vtable_address_comparisons)] + Rc::ptr_eq(reg, l) + }), + None => false, + } + }) + } + None => false, + }) + } + (Pending(lhs), Pending(rhs)) => { + if lhs.len() != rhs.len() { + false + } else { + use std::option::Option::None; + + lhs.iter() + .zip(rhs.iter()) + .all(|(lhs, rhs)| match (lhs, rhs) { + (Some(lhs), Some(rhs)) => + { + #[allow(clippy::vtable_address_comparisons)] + Rc::ptr_eq(lhs, rhs) + } + (None, None) => true, + _ => false, + }) + } + } + _ => false, + } + } +} + +impl Clone for Listeners { + fn clone(&self) -> Self { + match self { + Self::None | Self::Registered(_) => Self::None, + Self::Pending(v) => Self::Pending(v.clone()), + } + } +} + +impl Default for Listeners { + fn default() -> Self { + Self::None + } +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] +struct EventDescriptor { + kind: ListenerKind, + passive: bool, +} + +impl From<&dyn Listener> for EventDescriptor { + fn from(l: &dyn Listener) -> Self { + Self { + kind: l.kind(), + passive: l.passive(), + } + } +} + +/// Ensures global event handler registration. +// +// Separate struct to DRY, while avoiding partial struct mutability. +#[derive(Default, Debug)] +struct GlobalHandlers { + /// Events with registered handlers that are possibly passive + handling: HashSet, + + /// Keep track of all listeners to drop them on registry drop. + /// The registry is never dropped in production. + #[cfg(test)] + #[allow(clippy::type_complexity)] + registered: Vec<(ListenerKind, Closure)>, +} + +impl GlobalHandlers { + /// Ensure a descriptor has a global event handler assigned + fn ensure_handled(&mut self, desc: EventDescriptor) { + if !self.handling.contains(&desc) { + let cl = BODY.with(|body| { + let cl = Closure::wrap( + Box::new(move |e: Event| Registry::handle(desc, e)) as Box + ); + AsRef::::as_ref(body) + .add_event_listener_with_callback_and_add_event_listener_options( + &desc.kind.as_ref()[2..], + cl.as_ref().unchecked_ref(), + &{ + let mut opts = web_sys::AddEventListenerOptions::new(); + if desc.passive { + opts.passive(true); + } + opts + }, + ) + .map_err(|e| format!("could not register global listener: {:?}", e)) + .unwrap(); + cl + }); + + // Never drop the closure as this event handler is static + #[cfg(not(test))] + cl.forget(); + #[cfg(test)] + self.registered.push((desc.kind, cl)); + + self.handling.insert(desc); + } + } +} + +// Enable resetting between tests +#[cfg(test)] +impl Drop for GlobalHandlers { + fn drop(&mut self) { + BODY.with(|body| { + for (kind, cl) in std::mem::take(&mut self.registered) { + AsRef::::as_ref(body) + .remove_event_listener_with_callback( + &kind.as_ref()[2..], + cl.as_ref().unchecked_ref(), + ) + .unwrap(); + } + }); + } +} + +/// Global multiplexing event handler registry +#[derive(Default, Debug)] +struct Registry { + /// Counter for assigning new IDs + id_counter: u32, + + /// Registered global event handlers + global: GlobalHandlers, + + /// Contains all registered event listeners by listener ID + by_id: HashMap>>>, +} + +impl Registry { + /// Run f with access to global Registry + #[inline] + fn with(f: impl FnOnce(&mut Registry) -> R) -> R { + REGISTRY.with(|r| f(&mut *r.borrow_mut())) + } + + /// Register all passed listeners under ID + fn register(&mut self, id: u32, listeners: &[Option>]) { + let mut by_desc = + HashMap::>>::with_capacity(listeners.len()); + for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { + let desc = EventDescriptor::from(l.deref()); + self.global.ensure_handled(desc); + by_desc.entry(desc).or_default().push(l); + } + self.by_id.insert(id, by_desc); + } + + /// Patch an already registered set of handlers + fn patch(&mut self, id: &u32, listeners: &[Option>]) { + if let Some(by_desc) = self.by_id.get_mut(id) { + // Keeping empty vectors is fine. Those don't do much and should happen rarely. + for v in by_desc.values_mut() { + v.clear() + } + + for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { + let desc = EventDescriptor::from(l.deref()); + self.global.ensure_handled(desc); + by_desc.entry(desc).or_default().push(l); + } + } + } + + /// Unregister any existing listeners for ID + fn unregister(&mut self, id: &u32) { + self.by_id.remove(id); + } + + /// Set unique listener ID onto element and return it + fn set_listener_id(&mut self, el: &Element) -> u32 { + let id = self.id_counter; + self.id_counter += 1; + + LISTENER_ID_PROP.with(|prop| { + if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() { + panic!("failed to set listener ID property"); + } + }); + + id + } + + /// Handle a global event firing + fn handle(desc: EventDescriptor, event: Event) { + let target = match event + .target() + .map(|el| el.dyn_into::().ok()) + .flatten() + { + Some(el) => el, + None => return, + }; + + Self::run_handlers(desc, event, target); + } + + fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) { + let run_handler = |el: &web_sys::Element| { + if let Some(l) = LISTENER_ID_PROP + .with(|prop| js_sys::Reflect::get(el, prop).ok()) + .map(|v| v.dyn_into().ok()) + .flatten() + .map(|num: js_sys::Number| { + Registry::with(|r| { + r.by_id + .get(&(num.value_of() as u32)) + .map(|s| s.get(&desc)) + .flatten() + .cloned() + }) + }) + .flatten() + { + for l in l { + l.handle(event.clone()); + } + } + }; + + run_handler(&target); + + if unsafe { BUBBLE_EVENTS } { + let mut el = target; + loop { + el = match el.parent_element() { + Some(el) => el, + None => break, + }; + // XXX: we have no way to detect, if the callback called `Event.stopPropagation()` + // or `Event.stopImmediatePropagation()` without breaking the callback API. + // It's arguably not worth the cost. + run_handler(&el); + } + } + } +} + +#[cfg(all(test, feature = "wasm_test"))] +mod tests { + use std::marker::PhantomData; + + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + wasm_bindgen_test_configure!(run_in_browser); + + use crate::{html, html::TargetCast, utils::document, AppHandle, Component, Context, Html}; + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + #[derive(Clone)] + enum Message { + Click, + StopListening, + SetText(String), + } + + #[derive(Default)] + struct State { + stop_listening: bool, + clicked: u32, + text: String, + } + + trait Mixin { + fn passive() -> Option { + None + } + + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { + {state.clicked} + } + } else { + html! { + + {state.clicked} + + } + } + } + } + + struct Comp + where + M: Mixin + 'static, + { + state: State, + pd: PhantomData, + } + + impl Component for Comp + where + M: Mixin + 'static, + { + type Message = Message; + type Properties = (); + + fn create(_: &Context) -> Self { + Comp { + state: Default::default(), + pd: PhantomData, + } + } + + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + match msg { + Message::Click => { + self.state.clicked += 1; + } + Message::StopListening => { + self.state.stop_listening = true; + } + Message::SetText(s) => { + self.state.text = s; + } + }; + true + } + + fn view(&self, ctx: &Context) -> crate::Html { + M::view(ctx, &self.state) + } + } + + fn assert_count(el: &web_sys::HtmlElement, count: isize) { + assert_eq!(el.text_content(), Some(count.to_string())) + } + + fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { + document() + .query_selector(tag) + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + } + + fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) + where + M: Mixin, + { + // Remove any existing listeners and elements + super::Registry::with(|r| *r = Default::default()); + if let Some(el) = document().query_selector(tag).unwrap() { + el.parent_element().unwrap().remove(); + } + + let root = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&root).unwrap(); + let app = crate::start_app_in_element::>(root); + + (app, get_el_by_tag(tag)) + } + + #[test] + fn synchronous() { + struct Synchronous; + + impl Mixin for Synchronous {} + + let (link, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + + link.send_message(Message::StopListening); + el.click(); + assert_count(&el, 2); + } + + async fn await_animation_frame() { + JsFuture::from(js_sys::Promise::new(&mut |resolve, _| { + crate::utils::window() + .request_animation_frame(&resolve) + .unwrap(); + })) + .await + .unwrap(); + } + + #[test] + async fn passive() { + struct Passive; + + impl Mixin for Passive { + fn passive() -> Option { + Some(true) + } + } + + assert_async::().await; + } + + async fn assert_async() { + let (link, el) = init::("a"); + + macro_rules! assert_after_click { + ($c:expr) => { + el.click(); + await_animation_frame().await; + assert_count(&el, $c); + }; + } + + assert_count(&el, 0); + + assert_after_click!(1); + + assert_after_click!(2); + + link.send_message(Message::StopListening); + assert_after_click!(2); + } + + #[test] + fn bubbling() { + struct Bubbling; + + impl Mixin for Bubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { + + } + } else { + let cb = ctx.link().callback(|_| Message::Click); + html! { + + } + } + } + } + + let (link, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 2); + + el.click(); + assert_count(&el, 4); + + link.send_message(Message::StopListening); + el.click(); + assert_count(&el, 4); + } + + fn test_input_listener(make_event: impl Fn() -> E) + where + E: JsCast + std::fmt::Debug, + { + struct Input; + + impl Mixin for Input { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { +
+ +

{state.text.clone()}

+
+ } + } else { + html! { +
+ +

{state.text.clone()}

+
+ } + } + } + } + + let (link, input_el) = init::("input"); + let input_el = input_el.dyn_into::().unwrap(); + let p_el = get_el_by_tag("p"); + + assert_eq!(&p_el.text_content().unwrap(), ""); + for mut s in ["foo", "bar", "baz"].iter() { + input_el.set_value(s); + if s == &"baz" { + link.send_message(Message::StopListening); + s = &"bar"; + } + input_el + .dyn_ref::() + .unwrap() + .dispatch_event(&make_event().dyn_into().unwrap()) + .unwrap(); + assert_eq!(&p_el.text_content().unwrap(), s); + } + } + + #[test] + fn oninput() { + test_input_listener(|| { + web_sys::InputEvent::new_with_event_init_dict( + "input", + &web_sys::InputEventInit::new().bubbles(true), + ) + .unwrap() + }) + } + + #[test] + fn onchange() { + test_input_listener(|| { + web_sys::Event::new_with_event_init_dict( + "change", + &web_sys::EventInit::new().bubbles(true), + ) + .unwrap() + }) + } +} diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 4d5092d4aef..04d3f831193 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -3,6 +3,8 @@ #[doc(hidden)] pub mod key; #[doc(hidden)] +pub mod listeners; +#[doc(hidden)] pub mod vcomp; #[doc(hidden)] pub mod vlist; @@ -14,14 +16,15 @@ pub mod vtag; pub mod vtext; use crate::html::{AnyScope, NodeRef}; -use gloo::events::EventListener; use indexmap::IndexMap; -use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter}; +use std::{borrow::Cow, collections::HashMap, hint::unreachable_unchecked, iter}; use web_sys::{Element, Node}; #[doc(inline)] pub use self::key::Key; #[doc(inline)] +pub use self::listeners::*; +#[doc(inline)] pub use self::vcomp::{VChild, VComp}; #[doc(inline)] pub use self::vlist::VList; @@ -32,21 +35,6 @@ pub use self::vtag::VTag; #[doc(inline)] pub use self::vtext::VText; -/// The `Listener` trait is an universal implementation of an event listener -/// which is used to bind Rust-listener to JS-listener (DOM). -pub trait Listener { - /// Returns the name of the event - fn kind(&self) -> &'static str; - /// Attaches a listener to the element. - fn attach(&self, element: &Element) -> EventListener; -} - -impl fmt::Debug for dyn Listener { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Listener {{ kind: {} }}", self.kind()) - } -} - /// Attribute value pub type AttrValue = Cow<'static, str>; diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index e474a3e69a4..4d8e4df9f2e 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,9 +1,8 @@ //! This module contains the implementation of a virtual element node [VTag]. -use super::{Apply, AttrValue, Attributes, Key, Listener, VDiff, VList, VNode}; +use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VDiff, VList, VNode}; use crate::html::{AnyScope, IntoPropValue, NodeRef}; use crate::utils::document; -use gloo::events::EventListener; use log::warn; use std::borrow::Cow; use std::cmp::PartialEq; @@ -145,67 +144,6 @@ enum VTagInner { }, } -/// A list of event listeners, either registered or pending registration -/// TODO(#943): Compare references of handler to do listeners update better -#[derive(Debug)] -enum Listeners { - /// Listeners pending registration - Pending(Vec>), - - /// Already registered listeners. - /// Keeps handlers for attached listeners to have an opportunity to drop them later - Registered(Vec), -} - -impl Apply for Listeners { - type Element = Element; - - fn apply(&mut self, el: &Self::Element) { - if let Self::Pending(v) = self { - *self = Self::Registered( - std::mem::take(v) - .into_iter() - .map(|l| l.attach(el)) - .collect(), - ); - } - } - - fn apply_diff(&mut self, el: &Self::Element, _ancestor: Self) { - // All we need to do with `_ancestor` is drop it - - self.apply(el); - } -} - -impl PartialEq for Listeners { - fn eq(&self, other: &Self) -> bool { - use Listeners::*; - - match (self, other) { - (Pending(s), Pending(o)) => { - s.len() == o.len() && s.iter().map(|l| l.kind()).eq(o.iter().map(|l| l.kind())) - } - _ => false, - } - } -} - -impl Clone for Listeners { - fn clone(&self) -> Self { - match self { - Self::Pending(v) => Self::Pending(v.clone()), - Self::Registered(_) => Self::Registered(vec![]), - } - } -} - -impl From>> for Listeners { - fn from(v: Vec>) -> Self { - Self::Pending(v) - } -} - /// A type for a virtual /// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) /// representation. @@ -281,7 +219,7 @@ impl VTag { key: Option, // at bottom for more readable macro-expanded coded attributes: Attributes, - listeners: Vec>, + listeners: Listeners, ) -> Self { VTag::new_base( VTagInner::Input(InputFields { @@ -313,7 +251,7 @@ impl VTag { key: Option, // at bottom for more readable macro-expanded coded attributes: Attributes, - listeners: Vec>, + listeners: Listeners, ) -> Self { VTag::new_base( VTagInner::Textarea { @@ -340,7 +278,7 @@ impl VTag { key: Option, // at bottom for more readable macro-expanded coded attributes: Attributes, - listeners: Vec>, + listeners: Listeners, children: VList, ) -> Self { VTag::new_base( @@ -360,13 +298,13 @@ impl VTag { node_ref: NodeRef, key: Option, attributes: Attributes, - listeners: Vec>, + listeners: Listeners, ) -> Self { VTag { inner, reference: None, attributes, - listeners: listeners.into(), + listeners, node_ref, key, } @@ -497,22 +435,9 @@ impl VTag { .insert(key, value.into_prop_value()); } - /// Adds new listener to the node. - /// It's boxed because we want to keep it in a single list. - /// Later `Listener::attach` will attach an actual listener to a DOM node. - pub fn add_listener(&mut self, listener: Rc) { - if let Listeners::Pending(v) = &mut self.listeners { - v.push(listener); - } - } - - /// Adds new listeners to the node. - /// They are boxed because we want to keep them in a single list. - /// Later `Listener::attach` will attach an actual listener to a DOM node. - pub fn add_listeners(&mut self, listeners: Vec>) { - if let Listeners::Pending(v) = &mut self.listeners { - v.extend(listeners); - } + /// Set event listeners on the [VTag]'s [Element] + pub fn set_listener(&mut self, listeners: Box<[Option>]>) { + self.listeners = Listeners::Pending(listeners); } fn create_element(&self, parent: &Element) -> Element { @@ -542,6 +467,8 @@ impl VDiff for VTag { .take() .expect("tried to remove not rendered VTag from DOM"); + self.listeners.unregister(); + // recursively remove its children if let VTagInner::Other { children, .. } = &mut self.inner { children.detach(&node); @@ -1101,6 +1028,10 @@ mod tests { // check whether not changed virtual dom value has been set to the input element assert_eq!(current_value, "User input"); + + // Need to remove the element to clean up the dirty state of the DOM. Failing this causes + // event listener tests to fail. + parent.remove(); } #[test]