Dev/listener multiplexer (#1542)

* yew: partial event listener multiplexer

web_sys implementaion compiles but the std_web implementation is unfinished. Keeping this only to commit curretn progress before reverting std_web.

* yew: partial event listener multiplexer

Feature parity with master, except for bubbling.

* yew/listener: fix and test synchronous listeners

* yew/listener: add placeholder comments

* yew/listener: passive listener test

* yew: extend and fix APIs and docs

* yew/listener: event bubbling

* clippy: ignore warning

* Update yew/src/callback.rs

Co-authored-by: Simon <simon@siku2.io>

* Update yew/src/html/listener/listener_stdweb.rs

Co-authored-by: Simon <simon@siku2.io>

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* yew/listner: remove redundant function

* yew/listner: restore delibarate formatting

* yew/callback: make Flags a newtype

* yew/listener: use utility function

* yew/listener: deferred listeners

* yew/listner: input and change tests

* yew/listener: optimize listener registration

* yew/listener: remove benchmark placeholders

Seems easybench-wasm does not support specifying a module path.

* yew/callback: revert CallbackOnce -> Once

* yew: convert listener_tests to a build flag

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* yew: fix doc comments

* yew: simplify iteration

* yew/remove unneeded default passive listeners

* yew/listeners: DRY some more

* yew/listener: fix clippy warnings

* yew/listeners: remove legacy comment

* yew/listeners: document stopping propagation

* yew/listeners: update tests

* ci: see how test run on stable

* ci: let's find the new MSRV

* ci: try to run integration tests only on stable

* yew/test: clean up residual dirty state

* yew/listeners: minor doc string and inline fixes

* yew/listener: document reasonning for function

Co-authored-by: Simon <simon@siku2.io>
This commit is contained in:
Janis Petersons 2021-08-28 19:56:03 +03:00 committed by GitHub
parent 60c08736f1
commit 68d2fdbc59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1188 additions and 261 deletions

View File

@ -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

View File

@ -302,69 +302,68 @@ error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<Cow<'stat
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
= note: required by `into_prop_value`
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `{integer}`
error[E0277]: the trait bound `{integer}: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
--> $DIR/element-fail.rs:51:28
|
51 | html! { <input onclick=1 /> };
| ^ expected an `Fn<(MouseEvent,)>` closure, found `{integer}`
| ^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` 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<MouseEvent>` for `{integer}`
= help: the following implementations were found:
<&'static str as IntoPropValue<Cow<'static, str>>>
<&'static str as IntoPropValue<Option<Cow<'static, str>>>>
<&'static str as IntoPropValue<Option<String>>>
<&'static str as IntoPropValue<String>>
and 11 others
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback<String>`
error[E0277]: the trait bound `yew::Callback<String>: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
--> $DIR/element-fail.rs:52:29
|
52 | html! { <input onclick={Callback::from(|a: String| ())} /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| expected an implementor of trait `IntoEventCallback<MouseEvent>`
| help: consider borrowing here: `&Callback::from(|a: String| ())`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not implemented for `yew::Callback<String>`
|
::: $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<String>: IntoEventCallback<MouseEvent>` is not satisfied
= note: required because of the requirements on the impl of `IntoEventCallback<MouseEvent>` for `yew::Callback<String>`
error[E0277]: the trait bound `Option<{integer}>: IntoEventCallback<FocusEvent>` is not satisfied
error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<yew::Callback<FocusEvent>>>` is not satisfied
--> $DIR/element-fail.rs:53:29
|
53 | html! { <input onfocus={Some(5)} /> };
| ^^^^^^^ the trait `IntoEventCallback<FocusEvent>` is not implemented for `Option<{integer}>`
| ^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<FocusEvent>>>` 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:
<Option<T> as IntoEventCallback<EVENT>>
<Option<yew::Callback<EVENT>> as IntoEventCallback<EVENT>>
<Option<&'static str> as IntoPropValue<Option<Cow<'static, str>>>>
<Option<&'static str> as IntoPropValue<Option<String>>>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
error[E0277]: the trait bound `(): IntoPropValue<yew::NodeRef>` is not satisfied
--> $DIR/element-fail.rs:56:25
@ -386,28 +385,22 @@ error[E0277]: the trait bound `Option<yew::NodeRef>: IntoPropValue<yew::NodeRef>
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
= note: required by `into_prop_value`
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback<String>`
error[E0277]: the trait bound `yew::Callback<String>: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
--> $DIR/element-fail.rs:58:29
|
58 | html! { <input onclick={Callback::from(|a: String| ())} /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| expected an implementor of trait `IntoEventCallback<MouseEvent>`
| help: consider borrowing here: `&Callback::from(|a: String| ())`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not implemented for `yew::Callback<String>`
|
::: $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<String>: IntoEventCallback<MouseEvent>` is not satisfied
= note: required because of the requirements on the impl of `IntoEventCallback<MouseEvent>` for `yew::Callback<String>`
error[E0277]: the trait bound `NotToString: IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
--> $DIR/element-fail.rs:60:28

View File

@ -54,6 +54,7 @@ features = [
"Element",
"ErrorEvent",
"Event",
"EventInit",
"EventTarget",
"File",
"FileList",
@ -66,6 +67,7 @@ features = [
"HtmlSelectElement",
"HtmlTextAreaElement",
"InputEvent",
"InputEventInit",
"KeyboardEvent",
"Location",
"MessageEvent",

View File

@ -17,8 +17,18 @@ use std::rc::Rc;
/// </aside>
/// An `Rc` wrapper is used to make it cloneable.
pub enum Callback<IN> {
/// A callback which can be called multiple times
Callback(Rc<dyn Fn(IN)>),
/// A callback which can be called multiple times with optional modifier flags
Callback {
/// A callback which can be called multiple times
cb: Rc<dyn Fn(IN)>,
/// 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<bool>,
},
/// A callback which can only be called once. The callback will panic if it is
/// called more than once.
CallbackOnce(Rc<CallbackOnce<IN>>),
@ -28,14 +38,20 @@ type CallbackOnce<IN> = RefCell<Option<Box<dyn FnOnce(IN)>>>;
impl<IN, F: Fn(IN) + 'static> From<F> for Callback<IN> {
fn from(func: F) -> Self {
Callback::Callback(Rc::new(func))
Callback::Callback {
cb: Rc::new(func),
passive: None,
}
}
}
impl<IN> Clone for Callback<IN> {
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<IN> Clone for Callback<IN> {
impl<IN> PartialEq for Callback<IN> {
fn eq(&self, other: &Callback<IN>) -> 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<IN> PartialEq for Callback<IN> {
impl<IN> fmt::Debug for Callback<IN> {
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<IN> Callback<IN> {
/// 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)
}
};

View File

@ -17,7 +17,7 @@ pub struct ContextProviderProps<T: Clone + PartialEq> {
/// 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<T: Clone + PartialEq + 'static> {

View File

@ -255,6 +255,29 @@ impl<COMP: Component> Scope<COMP> {
/// synchronously schedules a call to the [Component](Component)
/// interface.
pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
where
M: Into<COMP::Message>,
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<F, IN, M>(
&self,
passive: impl Into<Option<bool>>,
function: F,
) -> Callback<IN>
where
M: Into<COMP::Message>,
F: Fn(IN) -> M + 'static,
@ -264,7 +287,10 @@ impl<COMP: Component> Scope<COMP> {
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

View File

@ -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 }
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;
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 }
/// A wrapper for a callback which attaches event listeners to elements.
#[derive(Clone, Debug)]
pub struct Wrapper {
callback: Callback<Event>,
}
impl Wrapper {
/// Create a wrapper for an event-typed callback
pub fn new(callback: Callback<Event>) -> Self {
Wrapper { callback }
}
#[doc(hidden)]
#[inline]
pub fn __macro_new(
callback: impl crate::html::IntoPropValue<Option<Callback<Event>>>,
) -> Option<Rc<dyn Listener>> {
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)
}

View File

@ -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<T>(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

View File

@ -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,

View File

@ -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<Registry> = 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<str> 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<Rc<dyn Listener>>]>),
}
impl Listeners {
/// Register listeners and return their handle ID
fn register(el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> 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<EventDescriptor>,
/// 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<dyn Fn(web_sys::Event)>)>,
}
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<dyn Fn(Event)>
);
AsRef::<web_sys::EventTarget>::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::<web_sys::EventTarget>::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<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>,
}
impl Registry {
/// Run f with access to global Registry
#[inline]
fn with<R>(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<Rc<dyn Listener>>]) {
let mut by_desc =
HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::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<Rc<dyn Listener>>]) {
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::<web_sys::Element>().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<bool> {
None
}
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<a>{state.clicked}</a>
}
} else {
html! {
<a onclick={ctx.link().callback_with_passive(
Self::passive(),
|_| Message::Click,
)}>
{state.clicked}
</a>
}
}
}
}
struct Comp<M>
where
M: Mixin + 'static,
{
state: State,
pd: PhantomData<M>,
}
impl<M> Component for Comp<M>
where
M: Mixin + 'static,
{
type Message = Message;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
Comp {
state: Default::default(),
pd: PhantomData,
}
}
fn update(&mut self, _: &Context<Self>, 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<Self>) -> 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::<web_sys::HtmlElement>()
.unwrap()
}
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, 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::<Comp<M>>(root);
(app, get_el_by_tag(tag))
}
#[test]
fn synchronous() {
struct Synchronous;
impl Mixin for Synchronous {}
let (link, el) = init::<Synchronous>("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<bool> {
Some(true)
}
}
assert_async::<Passive>().await;
}
async fn assert_async<M: Mixin + 'static>() {
let (link, el) = init::<M>("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<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<a>
{state.clicked}
</a>
</div>
}
} else {
let cb = ctx.link().callback(|_| Message::Click);
html! {
<div onclick={cb.clone()}>
<a onclick={cb}>
{state.clicked}
</a>
</div>
}
}
}
}
let (link, el) = init::<Bubbling>("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<E>(make_event: impl Fn() -> E)
where
E: JsCast + std::fmt::Debug,
{
struct Input;
impl Mixin for Input {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<input type="text" />
<p>{state.text.clone()}</p>
</div>
}
} else {
html! {
<div>
<input
type="text"
onchange={ctx.link().callback(|e: web_sys::Event| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
Message::SetText(el.value())
})}
oninput={ctx.link().callback(|e: web_sys::InputEvent| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
Message::SetText(el.value())
})}
/>
<p>{state.text.clone()}</p>
</div>
}
}
}
}
let (link, input_el) = init::<Input>("input");
let input_el = input_el.dyn_into::<web_sys::HtmlInputElement>().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::<web_sys::EventTarget>()
.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()
})
}
}

View File

@ -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>;

View File

@ -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<Rc<dyn Listener>>),
/// Already registered listeners.
/// Keeps handlers for attached listeners to have an opportunity to drop them later
Registered(Vec<EventListener>),
}
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<Vec<Rc<dyn Listener>>> for Listeners {
fn from(v: Vec<Rc<dyn Listener>>) -> 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<Key>,
// at bottom for more readable macro-expanded coded
attributes: Attributes,
listeners: Vec<Rc<dyn Listener>>,
listeners: Listeners,
) -> Self {
VTag::new_base(
VTagInner::Input(InputFields {
@ -313,7 +251,7 @@ impl VTag {
key: Option<Key>,
// at bottom for more readable macro-expanded coded
attributes: Attributes,
listeners: Vec<Rc<dyn Listener>>,
listeners: Listeners,
) -> Self {
VTag::new_base(
VTagInner::Textarea {
@ -340,7 +278,7 @@ impl VTag {
key: Option<Key>,
// at bottom for more readable macro-expanded coded
attributes: Attributes,
listeners: Vec<Rc<dyn Listener>>,
listeners: Listeners,
children: VList,
) -> Self {
VTag::new_base(
@ -360,13 +298,13 @@ impl VTag {
node_ref: NodeRef,
key: Option<Key>,
attributes: Attributes,
listeners: Vec<Rc<dyn Listener>>,
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<dyn Listener>) {
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<Rc<dyn Listener>>) {
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<Rc<dyn Listener>>]>) {
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]