From c1cd37da2df8597e45e8d62740ece7ba053136ae Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 30 Aug 2020 12:52:09 +0300 Subject: [PATCH 01/37] 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/src/callback.rs | 29 ++- yew/src/html/listener/listener_stdweb.rs | 166 +++++++-------- yew/src/html/listener/listener_web_sys.rs | 230 +++++++++++--------- yew/src/html/listener/macros.rs | 95 ++++++--- yew/src/html/listener/mod.rs | 86 +++++--- yew/src/html/listener/registry.rs | 243 ++++++++++++++++++++++ yew/src/html/scope.rs | 50 ++++- yew/src/virtual_dom/mod.rs | 83 +++++++- yew/src/virtual_dom/vtag.rs | 57 ++--- 9 files changed, 734 insertions(+), 305 deletions(-) create mode 100644 yew/src/html/listener/registry.rs diff --git a/yew/src/callback.rs b/yew/src/callback.rs index 375e6003bb4..0554be2352c 100644 --- a/yew/src/callback.rs +++ b/yew/src/callback.rs @@ -14,6 +14,23 @@ use std::rc::Rc; pub enum Callback { /// A callback which can be called multiple times Callback(Rc), + + /// A callback which can be called multiple times and has additional options for DOM + /// event listeners + CallbackWithOpts { + /// A callback which can be called multiple times + cb: Rc, + + /// Defines the event listener as passive. + /// Yew sets sane defaults depending on the type of the listener. + /// See [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEvent. + passive: bool, + + /// Defines event listener to also listen to events in the child tree that bubbled up to + /// the target element + handle_bubbled: bool, + }, + /// A callback which can only be called once. The callback will panic if it is /// called more than once. CallbackOnce(Rc>), @@ -31,6 +48,15 @@ impl Clone for Callback { fn clone(&self) -> Self { match self { Callback::Callback(cb) => Callback::Callback(cb.clone()), + Callback::CallbackWithOpts { + cb, + passive, + handle_bubbled, + } => Callback::CallbackWithOpts { + cb: cb.clone(), + passive: *passive, + handle_bubbled: *handle_bubbled, + }, Callback::CallbackOnce(cb) => Callback::CallbackOnce(cb.clone()), } } @@ -53,6 +79,7 @@ impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let data = match self { Callback::Callback(_) => "Callback<_>", + Callback::CallbackWithOpts { .. } => "CallbackWithOpts<_>", Callback::CallbackOnce(_) => "CallbackOnce<_>", }; @@ -64,7 +91,7 @@ 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) | Callback::CallbackWithOpts { cb, .. } => cb(value), Callback::CallbackOnce(rc) => { let cb = rc.replace(None); let f = cb.expect("callback in CallbackOnce has already been used"); diff --git a/yew/src/html/listener/listener_stdweb.rs b/yew/src/html/listener/listener_stdweb.rs index fed74c72114..9300a57654e 100644 --- a/yew/src/html/listener/listener_stdweb.rs +++ b/yew/src/html/listener/listener_stdweb.rs @@ -1,101 +1,77 @@ // Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events // Yew maintains a list of event listeners not supported by stdweb in the yew-macro crate (in the tag_attribute.rs file). // Be sure to update that list when you change anything here. -impl_action! { - onabort(event: ResourceAbortEvent) -> ResourceAbortEvent => |_, event| { event } - onauxclick(event: AuxClickEvent) -> AuxClickEvent => |_, event| { event } - onblur(event: BlurEvent) -> BlurEvent => |_, event| { event } - // oncancel not supported - // oncanplay not supported - // oncanplaythrough not supported - onchange(event: ChangeEvent) -> ChangeData => |this: &Element, _| { onchange_handler(this) } - onclick(event: ClickEvent) -> ClickEvent => |_, event| { event } - // onclose not supported - oncontextmenu(event: ContextMenuEvent) -> ContextMenuEvent => |_, event| { event } - // oncuechange not supported - ondblclick(event: DoubleClickEvent) -> DoubleClickEvent => |_, event| { event } - ondrag(event: DragEvent) -> DragEvent => |_, event| { event } - ondragend(event: DragEndEvent) -> DragEndEvent => |_, event| { event } - ondragenter(event: DragEnterEvent) -> DragEnterEvent => |_, event| { event } - ondragexit(event: DragExitEvent) -> DragExitEvent => |_, event| { event } - ondragleave(event: DragLeaveEvent) -> DragLeaveEvent => |_, event| { event } - ondragover(event: DragOverEvent) -> DragOverEvent => |_, event| { event } - ondragstart(event: DragStartEvent) -> DragStartEvent => |_, event| { event } - ondrop(event: DragDropEvent) -> DragDropEvent => |_, event| { event } - // ondurationchange not supported - // onemptied not supported - // onended not supported - onerror(event: ResourceErrorEvent) -> ResourceErrorEvent => |_, event| { event } - onfocus(event: FocusEvent) -> FocusEvent => |_, event| { event } - // onformdata not supported - oninput(event: InputEvent) -> InputData => |this: &Element, event| { oninput_handler(this, event) } - // oninvalid not supported - onkeydown(event: KeyDownEvent) -> KeyDownEvent => |_, event| { event } - onkeypress(event: KeyPressEvent) -> KeyPressEvent => |_, event| { event } - onkeyup(event: KeyUpEvent) -> KeyUpEvent => |_, event| { event } - onload(event: ResourceLoadEvent) -> ResourceLoadEvent => |_, event| { event } - // onloadeddata not supported - // onloadedmetadata not supported - onloadstart(event: LoadStartEvent) -> LoadStartEvent => |_, event| { event } - onmousedown(event: MouseDownEvent) -> MouseDownEvent => |_, event| { event } - onmouseenter(event: MouseEnterEvent) -> MouseEnterEvent => |_, event| { event } - onmouseleave(event: MouseLeaveEvent) -> MouseLeaveEvent => |_, event| { event } - onmousemove(event: MouseMoveEvent) -> MouseMoveEvent => |_, event| { event } - onmouseout(event: MouseOutEvent) -> MouseOutEvent => |_, event| { event } - onmouseover(event: MouseOverEvent) -> MouseOverEvent => |_, event| { event } - onmouseup(event: MouseUpEvent) -> MouseUpEvent => |_, event| { event } - // onpause not supported - // onplay not supported - // onplaying not supported - onprogress(event: ProgressEvent) -> ProgressEvent => |_, event| { event } - // onratechange not supported - // onreset not supported - onresize(event: ResizeEvent) -> ResizeEvent => |_, event| { event } - onscroll(event: ScrollEvent) -> ScrollEvent => |_, event| { event } - // onsecuritypolicyviolation not supported - // onseeked not supported - // onseeking not supported - // onselect not supported - onslotchange(event: SlotChangeEvent) -> SlotChangeEvent => |_, event| { event } - // onstalled not supported - onsubmit(event: SubmitEvent) -> SubmitEvent => |_, event| { event } - // onsuspend not supported - // ontimeupdate not supported - // ontoggle not supported - // onvolumechange not supported - // onwaiting not supported - onwheel(event: MouseWheelEvent) -> MouseWheelEvent => |_, event| { event } - // oncopy not supported - // oncut not supported - // onpaste not supported +// Reduces repetition for common cases +macro_rules! impl_short { + ($($action:ident($type:ident))*) => { + impl_action! { + $( + $action($type) -> $type + => |e| crate::html::listener::convert_reference::<$type>(e).0 + )* + } + }; +} - // onanimationcancel not supported - // onanimationend not supported - // onanimationiteration not supported - // onanimationstart not supported - ongotpointercapture(event: GotPointerCaptureEvent) -> GotPointerCaptureEvent => |_, event| { event } - onloadend(event: LoadEndEvent) -> LoadEndEvent => |_, event| { event } - onlostpointercapture(event: LostPointerCaptureEvent) -> LostPointerCaptureEvent => |_, event| { event } - onpointercancel(event: PointerCancelEvent) -> PointerCancelEvent => |_, event| { event } - onpointerdown(event: PointerDownEvent) -> PointerDownEvent => |_, event| { event } - onpointerenter(event: PointerEnterEvent) -> PointerEnterEvent => |_, event| { event } - onpointerleave(event: PointerLeaveEvent) -> PointerLeaveEvent => |_, event| { event } - onpointerlockchange(event: PointerLockChangeEvent) -> PointerLockChangeEvent => |_, event| { event } - onpointerlockerror(event: PointerLockErrorEvent) -> PointerLockErrorEvent => |_, event| { event } - onpointermove(event: PointerMoveEvent) -> PointerMoveEvent => |_, event| { event } - onpointerout(event: PointerOutEvent) -> PointerOutEvent => |_, event| { event } - onpointerover(event: PointerOverEvent) -> PointerOverEvent => |_, event| { event } - onpointerup(event: PointerUpEvent) -> PointerUpEvent => |_, event| { event } - onselectionchange(event: SelectionChangeEvent) -> SelectionChangeEvent => |_, event| { event } - // onselectstart not supported - // onshow not supported - ontouchcancel(event: TouchCancel) -> TouchCancel => |_, event| { event } - ontouchend(event: TouchEnd) -> TouchEnd => |_, event| { event } - ontouchmove(event: TouchMove) -> TouchMove => |_, event| { event } - ontouchstart(event: TouchStart) -> TouchStart => |_, event| { event } - // ontransitioncancel not supported - // ontransitionend not supported - // ontransitionrun not supported - // ontransitionstart not supported +// No converter +impl_short! { + onabort(ResourceAbortEvent) + onauxclick(AuxClickEvent) + onblur(BlurEvent) + onclick(ClickEvent) + oncontextmenu(ContextMenuEvent) + ondblclick(DoubleClickEvent) + ondrag(DragEvent) + ondragend(DragEndEvent) + ondragenter(DragEnterEvent) + ondragexit(DragExitEvent) + ondragleave(DragLeaveEvent) + ondragover(DragOverEvent) + ondragstart(DragStartEvent) + ondrop(DragDropEvent) + onerror(ResourceErrorEvent) + onfocus(FocusEvent) + onkeydown(KeyDownEvent) + onkeypress(KeyPressEvent) + onkeyup(KeyUpEvent) + onload(ResourceLoadEvent) + onloadstart(LoadStartEvent) + onmousedown(MouseDownEvent) + onmouseenter(MouseEnterEvent) + onmouseleave(MouseLeaveEvent) + onmousemove(MouseMoveEvent) + onmouseout(MouseOutEvent) + onmouseover(MouseOverEvent) + onmouseup(MouseUpEvent) + onprogress(ProgressEvent) + onresize(ResizeEvent) + onscroll(ScrollEvent) + onslotchange(SlotChangeEvent) + onsubmit(SubmitEvent) + onwheel(MouseWheelEvent) + ongotpointercapture(GotPointerCaptureEvent) + onloadend(LoadEndEvent) + onlostpointercapture(LostPointerCaptureEvent) + onpointercancel(PointerCancelEvent) + onpointerdown(PointerDownEvent) + onpointerenter(PointerEnterEvent) + onpointerleave(PointerLeaveEvent) + onpointerlockchange(PointerLockChangeEvent) + onpointerlockerror(PointerLockErrorEvent) + onpointermove(PointerMoveEvent) + onpointerout(PointerOutEvent) + onpointerover(PointerOverEvent) + onpointerup(PointerUpEvent) + onselectionchange(SelectionChangeEvent) + ontouchcancel(TouchCancel) + ontouchend(TouchEnd) + ontouchmove(TouchMove) + ontouchstart(TouchStart) +} + +// With converter +impl_action! { + onchange(ChangeEvent) -> ChangeData => onchange_handler + oninput(InputEvent) -> InputData => oninput_handler } diff --git a/yew/src/html/listener/listener_web_sys.rs b/yew/src/html/listener/listener_web_sys.rs index 8504c4152d7..30cd2e592a5 100644 --- a/yew/src/html/listener/listener_web_sys.rs +++ b/yew/src/html/listener/listener_web_sys.rs @@ -1,100 +1,138 @@ // 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) -> ChangeData => |this: &Element, _| { onchange_handler(this) } - 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 } - // web_sys doesn't have a struct for `FormDataEvent` - onformdata(name: "formdata", event: Event) -> web_sys::Event => |_, event| { event } - oninput(name: "input", event: InputEvent) -> InputData => |this: &Element, event| { oninput_handler(this, 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 } - onsubmit(name: "submit", event: FocusEvent) -> web_sys::FocusEvent => |_, 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 } +// Reduces repetition for common cases +macro_rules! impl_short { + ($($action:ident)*) => { + impl_action! { + $( + $action(Event, false) -> web_sys::Event => |e| e + )* + } + }; + ($($action:ident($type:ident))*) => { + impl_action! { + $( + $action($type, false) -> web_sys::$type + => |e| wasm_bindgen::JsCast::dyn_into::(e).unwrap() + )* + } + }; + ($($action:ident($type:ident, $passive:literal))*) => { + impl_action! { + $( + $action($type, $passive) -> web_sys::$type + => |e| wasm_bindgen::JsCast::dyn_into::(e).unwrap() + )* + } + }; +} - 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 } +// 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 + oncopy + oncut + onpaste + onpointerlockchange + onpointerlockerror + onselectionchange + onselectstart + onshow +} + +// Specialized event type +impl_short! { + onauxclick(MouseEvent) + onblur(FocusEvent) + onclick(MouseEvent) + oncontextmenu(MouseEvent) + ondblclick(MouseEvent) + ondrag(DragEvent) + ondragend(DragEvent) + ondragenter(DragEvent) + ondragexit(DragEvent) + ondragleave(DragEvent) + ondragover(DragEvent) + ondragstart(DragEvent) + ondrop(DragEvent) + onfocus(FocusEvent) + onkeydown(KeyboardEvent) + onkeypress(KeyboardEvent) + onkeyup(KeyboardEvent) + onloadstart(ProgressEvent) + onmousedown(MouseEvent) + onmouseenter(MouseEvent) + onmouseleave(MouseEvent) + onmouseout(MouseEvent) + onmouseover(MouseEvent) + onmouseup(MouseEvent) + onprogress(ProgressEvent) + onsubmit(FocusEvent) + onwheel(WheelEvent) + onanimationcancel(AnimationEvent) + onanimationend(AnimationEvent) + onanimationiteration(AnimationEvent) + onanimationstart(AnimationEvent) + ongotpointercapture(PointerEvent) + onloadend(ProgressEvent) + onlostpointercapture(PointerEvent) + onpointercancel(PointerEvent) + onpointerdown(PointerEvent) + onpointerenter(PointerEvent) + onpointerleave(PointerEvent) + onpointerout(PointerEvent) + onpointerover(PointerEvent) + onpointerup(PointerEvent) + ontouchcancel(TouchEvent) + ontouchend(TouchEvent) + ontransitioncancel(TransitionEvent) + ontransitionend(TransitionEvent) + ontransitionrun(TransitionEvent) + ontransitionstart(TransitionEvent) +} + +// Best used with passive listeners, if you handle each and every event globally +impl_short! { + onmousemove(MouseEvent, true) + onscroll(Event, true) + onpointermove(PointerEvent, true) + ontouchmove(TouchEvent, true) + ontouchstart(TouchEvent, true) +} + +// More specialized cases +impl_action! { + onchange(Event) -> ChangeData => onchange_handler + oninput(InputEvent) -> InputData => oninput_handler } diff --git a/yew/src/html/listener/macros.rs b/yew/src/html/listener/macros.rs index f8b1357babf..ecaf648c237 100644 --- a/yew/src/html/listener/macros.rs +++ b/yew/src/html/listener/macros.rs @@ -1,14 +1,13 @@ #[macro_use] macro_rules! impl_action { - ($($action:ident(event: $type:ident) -> $ret:ty => $convert:expr)*) => {$( - impl_action!($action(name: "", event: $type) -> $ret => $convert); + ($($action:ident($type:ident) -> $ret:path => $convert:expr)*) => {$( + impl_action!($action($type, false) -> $ret => $convert); )*}; - ($($action:ident(name: $name:literal, event: $type:ident) -> $ret:ty => $convert:expr)*) => {$( + ($($action:ident($type:ident, $passive:literal) -> $ret:path => $convert:expr)*) => {$( /// An abstract implementation of a listener. #[doc(hidden)] pub mod $action { use cfg_if::cfg_if; - use cfg_match::cfg_match; use crate::callback::Callback; #[allow(unused_imports)] use crate::html::listener::*; @@ -18,9 +17,7 @@ macro_rules! impl_action { use stdweb::web::event::$type; use stdweb::web::{Element, IEventTarget}; } else if #[cfg(feature = "web_sys")] { - use gloo::events::{EventListener, EventListenerOptions}; - use wasm_bindgen::JsValue; - use web_sys::{$type as WebSysType, Element, EventTarget}; + use web_sys::Element; } } @@ -40,37 +37,75 @@ macro_rules! impl_action { /// And event type which keeps the returned type. pub type Event = $ret; + #[cfg(feature = "std_web")] + type Argument = stdweb::Reference; + #[cfg(feature = "web_sys")] + type Argument = web_sys::Event; + impl Listener for Wrapper { fn kind(&self) -> &'static str { stringify!($action) } - fn attach(&self, element: &Element) -> EventListener { - let this = element.clone(); - let callback = self.callback.clone(); - let listener = move | - #[cfg(feature = "std_web")] event: $type, - #[cfg(feature = "web_sys")] event: &web_sys::Event - | { - #[cfg(feature = "web_sys")] - let event: WebSysType = JsValue::from(event).into(); - callback.emit($convert(&this, event)); - }; - cfg_match! { - feature = "std_web" => EventListener(Some(element.add_event_listener(listener))), - feature = "web_sys" => ({ - // We should only set passive event listeners for `touchstart` and `touchmove`. - // See here: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners - if $name == "touchstart" || $name == "touchmove" { - EventListener::new(&EventTarget::from(element.clone()), $name, listener) - } else { - let options = EventListenerOptions::enable_prevent_default(); - EventListener::new_with_options(&EventTarget::from(element.clone()), $name, options, listener) - } - }), + fn attach(&self, body: &Element, handler: Box) { + #[cfg(feature = "std_web")] + body.add_event_listener(move |event: $type| handler(event.into())); + + #[cfg(feature = "web_sys")] + crate::html::listener::macros::attach( + &self.kind()[2..], + body, + handler, + self.passive(), + ); + } + + fn handle(&self, event: Argument) { + self.callback.emit($convert(event)); + } + + #[cfg(feature = "web_sys")] + fn passive(&self) -> bool { + match &self.callback { + Callback::CallbackWithOpts{passive, ..} => *passive, + _ => $passive, + } + } + + fn handle_bubbled(&self) -> bool { + match &self.callback { + Callback::CallbackWithOpts{handle_bubbled, ..} => *handle_bubbled, + _ => false, } } } } )*}; } + +// Moved out to reduce instruction bloat +#[cfg(feature = "web_sys")] +pub(crate) fn attach( + kind: &str, + body: &web_sys::Element, + handler: Box, + passive: bool, +) { + use wasm_bindgen::prelude::*; + use wasm_bindgen::JsCast; + + let cl = Closure::wrap(handler); + AsRef::::as_ref(body) + .add_event_listener_with_callback_and_add_event_listener_options( + &kind[2..], + cl.as_ref().unchecked_ref(), + &{ + let mut opts = web_sys::AddEventListenerOptions::new(); + opts.passive(passive); + opts + }, + ) + .map_err(|e| format!("could not register global listener: {:?}", e)) + .unwrap(); + cl.forget(); // Never drop the closure as this event handler is static +} diff --git a/yew/src/html/listener/mod.rs b/yew/src/html/listener/mod.rs index 9faaaacff20..b888e52a805 100644 --- a/yew/src/html/listener/mod.rs +++ b/yew/src/html/listener/mod.rs @@ -1,5 +1,7 @@ #[macro_use] -mod macros; +pub(crate) mod macros; +mod registry; +pub(crate) use registry::*; use cfg_if::cfg_if; use cfg_match::cfg_match; @@ -8,11 +10,12 @@ cfg_if! { if #[cfg(feature = "std_web")] { mod listener_stdweb; + use stdweb::Reference as Event; use stdweb::js; - use stdweb::unstable::{TryFrom, TryInto}; use stdweb::web::html_element::{InputElement, SelectElement, TextAreaElement}; - use stdweb::web::{Element, EventListenerHandle, FileList, IElement, INode}; + use stdweb::web::{Element, FileList, IElement, INode}; use stdweb::web::event::InputEvent; + use std::convert::{TryInto, TryFrom}; pub use listener_stdweb::*; } else if #[cfg(feature = "web_sys")] { @@ -22,7 +25,8 @@ cfg_if! { use web_sys::{ Element, FileList, HtmlInputElement as InputElement, HtmlSelectElement as SelectElement, HtmlTextAreaElement as TextAreaElement, - InputEvent + InputEvent, + Event, }; pub use listener_web_sys::*; @@ -59,7 +63,39 @@ pub enum ChangeData { Files(FileList), } -fn oninput_handler(this: &Element, event: InputEvent) -> InputData { +/// Covert reference to target event type and element +#[cfg(feature = "std_web")] +pub(crate) fn convert_reference(obj: stdweb::Reference) -> (E, Element) +where + E: TryFrom, +{ + let event = E::try_from(obj).unwrap(); + (event, event.target().expect("no target on event")) +} + +/// Extract target as Element from event +#[cfg(feature = "web_sys")] +fn extract_target(event: &web_sys::Event) -> Element { + event + .target() + .expect("no target on event") + .dyn_into() + .unwrap() +} + +/// Covert reference to target event type and element +#[cfg(feature = "web_sys")] +pub(crate) fn convert_reference(event: web_sys::Event) -> (E, Element) +where + E: wasm_bindgen::JsCast, +{ + let target = extract_target(&event); + (event.dyn_into::().unwrap(), target) +} + +fn oninput_handler(event: Event) -> InputData { + let (event, this) = convert_reference::(event); + // Normally only InputElement or TextAreaElement can have an oninput event listener. In // practice though any element with `contenteditable=true` may generate such events, // therefore here we fall back to just returning the text content of the node. @@ -67,12 +103,10 @@ fn oninput_handler(this: &Element, event: InputEvent) -> InputData { let (v1, v2) = cfg_match! { feature = "std_web" => ({ ( - this.clone() - .try_into() + this.try_into() .map(|input: InputElement| input.raw_value()) .ok(), - this.clone() - .try_into() + this.try_into() .map(|input: TextAreaElement| input.value()) .ok(), ) @@ -90,12 +124,18 @@ fn oninput_handler(this: &Element, event: InputEvent) -> InputData { InputData { value, event } } -fn onchange_handler(this: &Element) -> ChangeData { +fn onchange_handler(event: Event) -> ChangeData { + #[cfg(feature = "std_web")] + let this = convert_reference::(event).1; + + #[cfg(feature = "web_sys")] + let this = extract_target(&event); + match this.node_name().as_ref() { "INPUT" => { let input = cfg_match! { - feature = "std_web" => InputElement::try_from(this.clone()).unwrap(), - feature = "web_sys" => this.dyn_ref::().unwrap(), + feature = "std_web" => InputElement::try_from(this).unwrap(), + feature = "web_sys" => this.dyn_into::().unwrap(), }; let is_file = input .get_attribute("type") @@ -116,15 +156,15 @@ fn onchange_handler(this: &Element) -> ChangeData { } "TEXTAREA" => { let tae = cfg_match! { - feature = "std_web" => TextAreaElement::try_from(this.clone()).unwrap(), - feature = "web_sys" => this.dyn_ref::().unwrap(), + feature = "std_web" => TextAreaElement::try_from(this).unwrap(), + feature = "web_sys" => this.dyn_into::().unwrap(), }; ChangeData::Value(tae.value()) } "SELECT" => { let se = cfg_match! { - feature = "std_web" => SelectElement::try_from(this.clone()).unwrap(), - feature = "web_sys" => this.dyn_ref::().unwrap().clone(), + feature = "std_web" => SelectElement::try_from(this).unwrap(), + feature = "web_sys" => this.dyn_into::().unwrap(), }; ChangeData::Select(se) } @@ -133,17 +173,3 @@ fn onchange_handler(this: &Element) -> ChangeData { } } } - -/// Handler to an event listener, only use is to cancel the event. -#[cfg(feature = "std_web")] -#[derive(Debug)] -pub struct EventListener(Option); - -#[cfg(feature = "std_web")] -impl Drop for EventListener { - fn drop(&mut self) { - if let Some(event) = self.0.take() { - event.remove() - } - } -} diff --git a/yew/src/html/listener/registry.rs b/yew/src/html/listener/registry.rs new file mode 100644 index 00000000000..9fab1decb49 --- /dev/null +++ b/yew/src/html/listener/registry.rs @@ -0,0 +1,243 @@ +use crate::virtual_dom::{Listener, Listeners}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +use cfg_if::cfg_if; +use cfg_match::cfg_match; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::Reference as Event; + use stdweb::web::Element; + } else if #[cfg(feature = "web_sys")] { + use wasm_bindgen::JsCast; + use web_sys::{Event, Element}; + } +} + +thread_local! { + static REGISTRY: Rc> = Default::default(); + + /// Key used to store listener id on element + #[cfg(feature = "web_sys")] + static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); +} + +#[cfg(feature = "std_web")] +const LISTENER_ID_PROP: &str = "data-yew-listener-id"; + +#[derive(Clone, Copy, std::hash::Hash, Eq, PartialEq)] +struct EventDescriptor { + kind: &'static str, + passive: bool, +} + +impl From<&dyn Listener> for EventDescriptor { + fn from(l: &dyn Listener) -> Self { + Self { + kind: l.kind(), + passive: cfg_match! { + feature = "std_web" => false, + feature = "web_sys" => l.passive(), + }, + } + } +} + +/// Global multiplexing event handler registry +#[derive(Default)] +struct Registry { + /// Counter for assigning new IDs + id_counter: u64, + + /// Events with registered handlers that are possibly passive + handling: HashSet, + + /// Events that have been registered as bubbling at least once + bubbling: HashSet, + + /// Contains all registered event listeners by listener ID + by_id: HashMap>>, +} + +impl Registry { + /// Run f with access to global Registry + fn with(mut f: impl FnMut(&mut Registry) -> R) -> R { + REGISTRY.with(|r| f(&mut *r.borrow_mut())) + } + + /// Register all passed listeners under ID + fn register(&mut self, id: u64, listeners: Vec>) { + let mut by_id = HashMap::with_capacity(listeners.len()); + for l in listeners.into_iter() { + // Create global listener, if not yet created + let key = EventDescriptor::from(&*l); + + if !self.handling.contains(&key) { + let cl = Box::new(move |e: Event| { + Registry::with(move |reg| reg.handle(&key, e.clone())) + }) as Box; + + #[cfg(feature = "web_sys")] + { + thread_local! { + static BODY: web_sys::HtmlElement = web_sys::window() + .expect("no window global") + .document() + .expect("no document on window") + .body() + .expect("no body on document"); + }; + + BODY.with(|b| l.attach(b, cl)); + } + + #[cfg(feature = "std_web")] + l.attach(&stdweb::web::document().body().unwrap().into(), cl); + + self.handling.insert(key); + } + + if l.handle_bubbled() { + self.bubbling.insert(key); + } + + by_id.insert(key, l); + } + self.by_id.insert(id, by_id); + } + + /// Unregister any existing listeners for ID + fn unregister(&mut self, id: &u64) { + self.by_id.remove(id); + } + + /// Set unique listener ID onto element and return it + fn set_listener_id(&mut self, el: &Element) -> u64 { + let id = self.id_counter; + self.id_counter += 1; + + #[cfg(feature = "web_sys")] + LISTENER_ID_PROP.with(|prop| { + if !js_sys::Reflect::set(el, &prop, &js_sys::JsString::from(id.to_string())).unwrap() { + panic!("failed to set listener ID property"); + } + }); + + // std_web does not support reflection so we have little choice but to pollute the + // attributes + #[cfg(feature = "std_web")] + el.set_attribute(LISTENER_ID_PROP, &id.to_string()).unwrap(); + + id + } + + /// Handle a global event firing + fn handle(&self, desc: &EventDescriptor, event: Event) { + if let Some(l) = event + .target() + .map(|el| { + cfg_match! { + feature = "std_web" => el.try_into::().ok(), + feature = "web_sys" => el.dyn_into::().ok(), + } + }) + .flatten() + .map(|el| { + cfg_match! { + feature = "std_web" => el.get_attribute(LISTENER_ID_PROP), + feature = "web_sys" => LISTENER_ID_PROP + .with(|prop| js_sys::Reflect::get(&el, &prop).ok()), + } + }) + .flatten() + .map(|v| { + cfg_match! { + feature = "std_web" => v.parse().ok(), + feature = "web_sys" => v.dyn_into().ok() + .map(|v: js_sys::JsString| String::from(v).parse().ok()), + } + }) + .flatten() + .flatten() + .map(|id: u64| self.by_id.get(&id).map(|s| s.get(desc)).flatten()) + .flatten() + { + l.handle(event); + + if self.bubbling.contains(desc) { + // TODO: if not passive, check if default was prevented after each call + // (including the first call above) + // TODO: travel up the parents for event bubbling + } + } + } +} + +/// Register new event listeners to the element +pub(crate) fn set_listeners(el: &Element, new: &mut Listeners) { + if let Listeners::Pending(ref mut pending) = new { + if pending.is_empty() { + return; + } + + *new = Listeners::Registered(Registry::with(|reg| { + let id = reg.set_listener_id(el); + reg.register(id, std::mem::take(pending)); + id + })); + } +} + +/// Register new event listeners to the element, replacing old ones +pub(crate) fn patch_listeners(mut new: &mut Listeners, old: &Listeners) { + if let (Listeners::Pending(pending), Listeners::Registered(id)) = (&mut new, old) { + Registry::with(|reg| { + // The listeners should never hav pointer equality so always replacing them is a slight + // optimisation + reg.unregister(id); + if pending.is_empty() { + return; + } + reg.register(*id, std::mem::take(pending)); + }); + *new = Listeners::Registered(*id); + } +} + +/// Remove any registered event listeners from the element. +/// The listener ID should be reused, if the element is reused. +pub(crate) fn remove_listeners(listeners: &Listeners) { + if let Listeners::Registered(id) = listeners { + Registry::with(|reg| reg.unregister(id)); + } +} + +/// Compare passed listeners for equality with a registered set via pointer equality checks +pub(crate) fn compare_listeners(registered_id: u64, rhs: &[Rc]) -> bool { + // Empty sets are not stored + if rhs.is_empty() { + return false; + } + + Registry::with(|reg| match reg.by_id.get(®istered_id) { + Some(reg) => { + if reg.len() != rhs.len() { + return false; + } + + rhs.iter() + .any(|l| match reg.get(&EventDescriptor::from(&**l)) { + Some(reg) => + { + #[allow(clippy::vtable_address_comparisons)] + !Rc::ptr_eq(reg, l) + } + None => true, + }) + } + None => false, + }) +} diff --git a/yew/src/html/scope.rs b/yew/src/html/scope.rs index b3be1688d7f..9272d3cda34 100644 --- a/yew/src/html/scope.rs +++ b/yew/src/html/scope.rs @@ -212,6 +212,18 @@ impl Scope { self.update(ComponentUpdate::MessageBatch(messages)); } + fn wrap_closure(&self, function: F) -> Rc + where + M: Into, + F: Fn(IN) -> M + 'static, + { + let scope = self.clone(); + Rc::new(move |input| { + let output = function(input); + scope.send_message(output); + }) + } + /// Creates a `Callback` which will send a message to the linked /// component's update method when invoked. /// @@ -223,12 +235,38 @@ impl Scope { M: Into, F: Fn(IN) -> M + 'static, { - let scope = self.clone(); - let closure = move |input| { - let output = function(input); - scope.send_message(output); - }; - closure.into() + Callback::Callback(self.wrap_closure(function)) + } + + /// Creates a `Callback` which will send a message to the linked + /// component's update method when invoked with additional options for DOM event + /// listeners, + /// + /// `passive` specifies a passive event listener to be created. Does nothing with + /// `feature = "std_web"` as std_web does not support passive listeners. Only kept to retain + /// some API coherence. + /// + /// `handle_bubble` specifies the event listener to also listen to events in the child + /// tree that bubbled up to the target element. + /// + /// Please be aware that currently the result of this callback + /// synchronously schedules a call to the [Component](Component) + /// interface. + pub fn callback_with_opts( + &self, + passive: bool, + handle_bubbled: bool, + function: F, + ) -> Callback + where + M: Into, + F: Fn(IN) -> M + 'static, + { + Callback::CallbackWithOpts { + passive, + handle_bubbled, + cb: self.wrap_closure(function), + } } /// Creates a `Callback` from an `FnOnce` which will send a message diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 79af7c4ca2c..1a0b0030810 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -21,11 +21,10 @@ use std::fmt; use std::rc::Rc; cfg_if! { if #[cfg(feature = "std_web")] { - use crate::html::EventListener; + use stdweb::Reference as Event; use stdweb::web::{Element, INode, Node}; } else if #[cfg(feature = "web_sys")] { - use gloo::events::EventListener; - use web_sys::{Element, Node}; + use web_sys::{Element, Event, Node}; } } @@ -47,18 +46,86 @@ pub use self::vtext::VText; 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; + + /// Attaches a listener to the document body + fn attach(&self, body: &Element, handler: Box); + + /// Handles an event firing + fn handle(&self, event: Event); + + /// Defines the event listener as passive. + /// See [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEvent. + #[cfg(feature = "web_sys")] + fn passive(&self) -> bool; + + /// Defines event listener to also listen to events in the child tree that bubbled up to + /// the target element + fn handle_bubbled(&self) -> bool; } impl fmt::Debug for dyn Listener { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Listener {{ kind: {} }}", self.kind()) + write!( + f, + "Listener {{ kind: {}, passive: {}, handle_bubbled: {} }}", + self.kind(), + self.passive(), + self.handle_bubbled() + ) + } +} + +/// A list of event listeners, either registered or pending registration +#[derive(Debug, Clone)] +pub enum Listeners { + /// Added to global registry by ID + Registered(u64), + + /// Not yet added to global registry + Pending(Vec>), +} + +impl Default for Listeners { + fn default() -> Self { + Self::Pending(Default::default()) + } +} + +impl PartialEq for Listeners { + fn eq(&self, rhs: &Self) -> bool { + use crate::html::compare_listeners; + use Listeners::*; + + match (self, rhs) { + (Registered(lhs), Registered(rhs)) => lhs == rhs, + (Registered(lhs), Pending(rhs)) => compare_listeners(*lhs, &rhs), + (Pending(lhs), Pending(rhs)) => { + if lhs.len() != rhs.len() { + return false; + } + + let mut lhs_it = lhs.iter(); + let mut rhs_it = rhs.iter(); + loop { + match (lhs_it.next(), rhs_it.next()) { + (Some(lhs), Some(rhs)) => + { + #[allow(clippy::vtable_address_comparisons)] + if !Rc::ptr_eq(lhs, rhs) { + return false; + } + } + (None, None) => return true, + _ => return false, + }; + } + } + (Pending(lhs), Registered(rhs)) => compare_listeners(*rhs, &lhs), + } } } -/// A list of event listeners. -type Listeners = Vec>; +impl Eq for Listeners {} /// A map of attributes. type Attributes = HashMap; diff --git a/yew/src/virtual_dom/vtag.rs b/yew/src/virtual_dom/vtag.rs index c6105c5e5e3..d6bedcf57f3 100644 --- a/yew/src/virtual_dom/vtag.rs +++ b/yew/src/virtual_dom/vtag.rs @@ -11,14 +11,12 @@ use std::cmp::PartialEq; use std::rc::Rc; cfg_if! { if #[cfg(feature = "std_web")] { - use crate::html::EventListener; #[allow(unused_imports)] use stdweb::{_js_impl, js}; use stdweb::unstable::TryFrom; use stdweb::web::html_element::{InputElement, TextAreaElement}; use stdweb::web::{Element, IElement, INode}; } else if #[cfg(feature = "web_sys")] { - use gloo::events::EventListener; use std::ops::Deref; use wasm_bindgen::JsCast; use web_sys::{ @@ -64,7 +62,7 @@ pub struct VTag { element_type: ElementType, /// A reference to the DOM `Element`. pub reference: Option, - /// List of attached listeners. + /// Either registered or pending event listeners pub listeners: Listeners, /// List of attributes. pub attributes: Attributes, @@ -85,8 +83,6 @@ pub struct VTag { pub checked: bool, /// A node reference used for DOM access in Component lifecycle methods pub node_ref: NodeRef, - /// Keeps handler for attached listeners to have an opportunity to drop them later. - captured: Vec, pub key: Option, } @@ -105,7 +101,6 @@ impl Clone for VTag { checked: self.checked, node_ref: self.node_ref.clone(), key: self.key.clone(), - captured: Vec::new(), } } } @@ -120,8 +115,7 @@ impl VTag { element_type, reference: None, attributes: Attributes::new(), - listeners: Vec::new(), - captured: Vec::new(), + listeners: Default::default(), children: VList::new(), node_ref: NodeRef::default(), key: None, @@ -186,34 +180,17 @@ impl VTag { } } - /// 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. + /// Adds a new listener to the node pub fn add_listener(&mut self, listener: Rc) { - self.listeners.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>) { - for listener in listeners { - self.listeners.push(listener); + if let Listeners::Pending(p) = &mut self.listeners { + p.push(listener) } } - /// Every render it removes all listeners and attach it back later - /// TODO(#943): Compare references of handler to do listeners update better - fn recreate_listeners(&mut self, ancestor: &mut Option>) { - if let Some(ancestor) = ancestor.as_mut() { - ancestor.captured.clear(); - } - - let element = self.reference.clone().expect("element expected"); - - for listener in self.listeners.drain(..) { - let handle = listener.attach(&element); - self.captured.push(handle); + /// Adds new listeners to the node + pub fn add_listeners(&mut self, listeners: Vec>) { + if let Listeners::Pending(p) = &mut self.listeners { + p.extend(listeners) } } @@ -447,6 +424,8 @@ impl VDiff for VTag { .take() .expect("tried to remove not rendered VTag from DOM"); + crate::html::remove_listeners(&self.listeners); + // recursively remove its children self.children.detach(&node); if parent.remove_child(&node).is_err() { @@ -463,6 +442,8 @@ impl VDiff for VTag { next_sibling: NodeRef, ancestor: Option, ) -> NodeRef { + use crate::html::{patch_listeners, set_listeners}; + let mut ancestor_tag = ancestor.and_then(|mut ancestor| { match ancestor { // If the ancestor is a tag of the same type, don't recreate, keep the @@ -471,6 +452,7 @@ impl VDiff for VTag { _ => { let element = self.create_element(parent); super::insert_node(&element, parent, Some(ancestor.first_node())); + set_listeners(&element, &mut self.listeners); self.reference = Some(element); ancestor.detach(parent); None @@ -482,16 +464,18 @@ impl VDiff for VTag { // Refresh the current value to later compare it against the desired value // since it may have been changed since we last set it. ancestor_tag.refresh_value(); + // Preserve the reference that already exists. self.reference = ancestor_tag.reference.take(); + patch_listeners(&mut self.listeners, &ancestor_tag.listeners); } else if self.reference.is_none() { let element = self.create_element(parent); super::insert_node(&element, parent, next_sibling.get()); + set_listeners(&element, &mut self.listeners); self.reference = Some(element); } self.apply_diffs(&ancestor_tag); - self.recreate_listeners(&mut ancestor_tag); // Process children let element = self.reference.as_ref().expect("Reference should be set"); @@ -529,13 +513,8 @@ impl PartialEq for VTag { && self.value == other.value && self.kind == other.kind && self.checked == other.checked - && self.listeners.len() == other.listeners.len() - && self - .listeners - .iter() - .map(|l| l.kind()) - .eq(other.listeners.iter().map(|l| l.kind())) && self.attributes == other.attributes + && self.listeners == other.listeners && self.children == other.children } } From 1de087e305f344ad22645772c026058c70c85d88 Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 30 Aug 2020 19:36:19 +0300 Subject: [PATCH 02/37] yew: partial event listener multiplexer Feature parity with master, except for bubbling. --- yew/src/callback.rs | 88 ++++--- yew/src/html/listener/listener_stdweb.rs | 168 ++++++++++++- yew/src/html/listener/listener_web_sys.rs | 138 ----------- yew/src/html/listener/listener_websys.rs | 273 ++++++++++++++++++++++ yew/src/html/listener/macros.rs | 111 --------- yew/src/html/listener/mod.rs | 143 +----------- yew/src/html/listener/registry.rs | 111 ++++----- yew/src/html/scope.rs | 45 ++-- yew/src/virtual_dom/mod.rs | 101 ++++---- yew/src/virtual_dom/vtag.rs | 27 +++ 10 files changed, 638 insertions(+), 567 deletions(-) delete mode 100644 yew/src/html/listener/listener_web_sys.rs create mode 100644 yew/src/html/listener/listener_websys.rs delete mode 100644 yew/src/html/listener/macros.rs diff --git a/yew/src/callback.rs b/yew/src/callback.rs index cef9c150956..706e827b588 100644 --- a/yew/src/callback.rs +++ b/yew/src/callback.rs @@ -4,6 +4,24 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; +/// Defines the event listener as passive. +/// Yew sets sane defaults depending on the type of the listener. +/// See [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEvent). +pub const PASSIVE: u8 = 1; +/// Causes the event handler to not fire until the next animation frame +/// Implies `PASSIVE`. +// TODO: this flag can apply to Agent event handling as well +pub const DEFER: u8 = 1 << 1 | PASSIVE; +/// Causes the event handler to not fire until the next animation frame and be called with the last +/// fired event. +/// Implies `PASSIVE` and `DEFER`. +// TODO: this flag can apply to Agent event handling as well +pub const DEBOUNCE: u8 = 1 << 2 | DEFER; +/// Defines event listener to also listen to events in the child tree that bubbled up to the target +/// element +#[cfg(feature = "web_sys")] +pub const HANDLE_BUBBLED: u8 = 1 << 3; + /// Universal callback wrapper. /// /// 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 and has additional options for DOM - /// event listeners - CallbackWithOpts { + /// A callback which can be called multiple times with optional flags + Callback { /// A callback which can be called multiple times cb: Rc, - /// Defines the event listener as passive. - /// Yew sets sane defaults depending on the type of the listener. - /// See [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEvent. - passive: bool, - - /// Defines event listener to also listen to events in the child tree that bubbled up to - /// the target element - handle_bubbled: bool, + /// Sets flags for event listening. A combination of `PASSIVE`, `DEBOUNCE`, `DEFER` and + /// `HANDLE_BUBBLED`. + /// + /// `DEFER` implies `PASSIVE`. + /// `DEBOUNCE` implies `PASSIVE` and `DEFER`. + /// + /// Currently only used with `feature = "web_sys"`. + flags: u8, }, /// A callback which can only be called once. The callback will panic if it is /// called more than once. - CallbackOnce(Rc>), + Once(Rc>), } -type CallbackOnce = RefCell>>; +type Once = RefCell>>; impl From for Callback { fn from(func: F) -> Self { - Callback::Callback(Rc::new(func)) + Callback::Callback { + cb: Rc::new(func), + flags: 0, + } } } impl Clone for Callback { fn clone(&self) -> Self { match self { - Callback::Callback(cb) => Callback::Callback(cb.clone()), - Callback::CallbackWithOpts { - cb, - passive, - handle_bubbled, - } => Callback::CallbackWithOpts { + Callback::Callback { cb, flags } => Callback::Callback { cb: cb.clone(), - passive: *passive, - handle_bubbled: *handle_bubbled, + flags: *flags, }, - Callback::CallbackOnce(cb) => Callback::CallbackOnce(cb.clone()), + Callback::Once(cb) => Callback::Once(cb.clone()), } } } @@ -66,10 +77,14 @@ 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::Once(cb), Callback::Once(other_cb)) => Rc::ptr_eq(cb, other_cb), + ( + Callback::Callback { cb, flags }, + Callback::Callback { + cb: rhs_cb, + flags: rhs_flags, + }, + ) => Rc::ptr_eq(cb, rhs_cb) && flags == rhs_flags, _ => false, } } @@ -78,9 +93,8 @@ 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::CallbackWithOpts { .. } => "CallbackWithOpts<_>", - Callback::CallbackOnce(_) => "CallbackOnce<_>", + Callback::Callback { .. } => "Callback<_>", + Callback::Once(_) => "Once<_>", }; f.write_str(data) @@ -91,10 +105,10 @@ impl Callback { /// This method calls the callback's function. pub fn emit(&self, value: IN) { match self { - Callback::Callback(cb) | Callback::CallbackWithOpts { cb, .. } => cb(value), - Callback::CallbackOnce(rc) => { + Callback::Callback { cb, .. } => cb(value), + Callback::Once(rc) => { let cb = rc.replace(None); - let f = cb.expect("callback in CallbackOnce has already been used"); + let f = cb.expect("callback in Once has already been used"); f(value) } }; @@ -107,7 +121,7 @@ impl Callback { where F: FnOnce(IN) + 'static, { - Callback::CallbackOnce(Rc::new(RefCell::new(Some(Box::new(func))))) + Callback::Once(Rc::new(RefCell::new(Some(Box::new(func))))) } /// Creates a "no-op" callback which can be used when it is not suitable to use an diff --git a/yew/src/html/listener/listener_stdweb.rs b/yew/src/html/listener/listener_stdweb.rs index 9300a57654e..110ffb8de8c 100644 --- a/yew/src/html/listener/listener_stdweb.rs +++ b/yew/src/html/listener/listener_stdweb.rs @@ -2,25 +2,138 @@ // Yew maintains a list of event listeners not supported by stdweb in the yew-macro crate (in the tag_attribute.rs file). // Be sure to update that list when you change anything here. -// Reduces repetition for common cases -macro_rules! impl_short { +use super::{ChangeData, InputData}; +use stdweb::js; +use stdweb::unstable::{TryFrom, TryInto}; +use stdweb::web::event::InputEvent; +use stdweb::web::html_element::{InputElement, SelectElement, TextAreaElement}; +use stdweb::web::{Element, EventListenerHandle, IElement, INode}; + +pub(crate) fn oninput_handler(this: &Element, event: InputEvent) -> InputData { + // Normally only InputElement or TextAreaElement can have an oninput event listener. In + // practice though any element with `contenteditable=true` may generate such events, + // therefore here we fall back to just returning the text content of the node. + // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event. + let value = this + .clone() + .try_into() + .map(|input: InputElement| input.raw_value()) + .ok() + .or_else(|| { + this.clone() + .try_into() + .map(|input: TextAreaElement| input.value()) + .ok() + }) + .or_else(|| this.text_content()) + .expect(concat!( + "only an InputElement or TextAreaElement or an element with contenteditable=true ", + "can have an oninput event listener" + )); + InputData { value, event } +} + +pub(crate) fn onchange_handler(this: &Element) -> ChangeData { + match this.node_name().as_ref() { + "INPUT" => { + let input = InputElement::try_from(this.clone()).unwrap(); + if input + .get_attribute("type") + .map(|value| value.eq_ignore_ascii_case("file")) + .unwrap_or(false) + { + ChangeData::Files(js!( return @{input}.files; ).try_into().unwrap()) + } else { + ChangeData::Value(input.raw_value()) + } + } + "TEXTAREA" => ChangeData::Value(TextAreaElement::try_from(this.clone()).unwrap().value()), + "SELECT" => ChangeData::Select(SelectElement::try_from(this.clone()).unwrap()), + _ => { + panic!(concat!( + "only an InputElement, TextAreaElement or SelectElement ", + "can have an onchange event listener" + )); + } + } +} + +/// Handler to an event listener, only use is to cancel the event. +#[derive(Debug)] +pub struct EventListener(pub(crate) Option); + +impl Drop for EventListener { + fn drop(&mut self) { + if let Some(event) = self.0.take() { + event.remove() + } + } +} + +#[macro_use] +macro_rules! impl_action { ($($action:ident($type:ident))*) => { impl_action! { $( - $action($type) -> $type - => |e| crate::html::listener::convert_reference::<$type>(e).0 + $action($type) -> $type => |_, event| { event } )* } }; + ($($action:ident($type:ident) -> $ret:ty => $convert:expr)*) => {$( + /// An abstract implementation of a listener. + #[doc(hidden)] + pub mod $action { + use crate::callback::Callback; + use crate::html::listener::EventListener; + use crate::virtual_dom::Listener; + use stdweb::web::event::$type; + use stdweb::web::{Element, IEventTarget}; + + /// 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 } + } + } + + /// And event type which keeps the returned type. + pub type Event = $ret; + + impl Listener for Wrapper { + fn kind(&self) -> &'static str { + stringify!($action) + } + + fn attach(&self, element: &Element) -> EventListener { + let this = element.clone(); + let callback = self.callback.clone(); + EventListener(Some(element.add_event_listener( + move |event: $type | callback.emit($convert(&this, event))) + )) + } + } + } + )*}; } -// No converter -impl_short! { +// No conversion +impl_action! { onabort(ResourceAbortEvent) onauxclick(AuxClickEvent) onblur(BlurEvent) + // oncancel not supported + // oncanplay not supported + // oncanplaythrough not supported onclick(ClickEvent) + // onclose not supported oncontextmenu(ContextMenuEvent) + // oncuechange not supported ondblclick(DoubleClickEvent) ondrag(DragEvent) ondragend(DragEndEvent) @@ -30,12 +143,19 @@ impl_short! { ondragover(DragOverEvent) ondragstart(DragStartEvent) ondrop(DragDropEvent) + // ondurationchange not supported + // onemptied not supported + // onended not supported onerror(ResourceErrorEvent) onfocus(FocusEvent) + // onformdata not supported + // oninvalid not supported onkeydown(KeyDownEvent) onkeypress(KeyPressEvent) onkeyup(KeyUpEvent) onload(ResourceLoadEvent) + // onloadeddata not supported + // onloadedmetadata not supported onloadstart(LoadStartEvent) onmousedown(MouseDownEvent) onmouseenter(MouseEnterEvent) @@ -44,12 +164,34 @@ impl_short! { onmouseout(MouseOutEvent) onmouseover(MouseOverEvent) onmouseup(MouseUpEvent) + // onpause not supported + // onplay not supported + // onplaying not supported onprogress(ProgressEvent) + // onratechange not supported + // onreset not supported onresize(ResizeEvent) onscroll(ScrollEvent) + // onsecuritypolicyviolation not supported + // onseeked not supported + // onseeking not supported + // onselect not supported onslotchange(SlotChangeEvent) + // onstalled not supported onsubmit(SubmitEvent) + // onsuspend not supported + // ontimeupdate not supported + // ontoggle not supported + // onvolumechange not supported + // onwaiting not supported onwheel(MouseWheelEvent) + // oncopy not supported + // oncut not supported + // onpaste not supported + // onanimationcancel not supported + // onanimationend not supported + // onanimationiteration not supported + // onanimationstart not supported ongotpointercapture(GotPointerCaptureEvent) onloadend(LoadEndEvent) onlostpointercapture(LostPointerCaptureEvent) @@ -64,14 +206,22 @@ impl_short! { onpointerover(PointerOverEvent) onpointerup(PointerUpEvent) onselectionchange(SelectionChangeEvent) + // onselectstart not supported + // onshow not supported ontouchcancel(TouchCancel) ontouchend(TouchEnd) ontouchmove(TouchMove) ontouchstart(TouchStart) + // ontransitioncancel not supported + // ontransitionend not supported + // ontransitionrun not supported + // ontransitionstart not supported } -// With converter +// With payload conversion impl_action! { - onchange(ChangeEvent) -> ChangeData => onchange_handler - oninput(InputEvent) -> InputData => oninput_handler + onchange(ChangeEvent) -> crate::html::listener::ChangeData + => |this: &Element, _| { crate::html::listener::onchange_handler(this) } + oninput(InputEvent) -> crate::html::listener::InputData + => |this: &Element, event| { crate::html::listener::oninput_handler(this, event) } } diff --git a/yew/src/html/listener/listener_web_sys.rs b/yew/src/html/listener/listener_web_sys.rs deleted file mode 100644 index 30cd2e592a5..00000000000 --- a/yew/src/html/listener/listener_web_sys.rs +++ /dev/null @@ -1,138 +0,0 @@ -// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events - -// Reduces repetition for common cases -macro_rules! impl_short { - ($($action:ident)*) => { - impl_action! { - $( - $action(Event, false) -> web_sys::Event => |e| e - )* - } - }; - ($($action:ident($type:ident))*) => { - impl_action! { - $( - $action($type, false) -> web_sys::$type - => |e| wasm_bindgen::JsCast::dyn_into::(e).unwrap() - )* - } - }; - ($($action:ident($type:ident, $passive:literal))*) => { - impl_action! { - $( - $action($type, $passive) -> web_sys::$type - => |e| wasm_bindgen::JsCast::dyn_into::(e).unwrap() - )* - } - }; -} - -// 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 - oncopy - oncut - onpaste - onpointerlockchange - onpointerlockerror - onselectionchange - onselectstart - onshow -} - -// Specialized event type -impl_short! { - onauxclick(MouseEvent) - onblur(FocusEvent) - onclick(MouseEvent) - oncontextmenu(MouseEvent) - ondblclick(MouseEvent) - ondrag(DragEvent) - ondragend(DragEvent) - ondragenter(DragEvent) - ondragexit(DragEvent) - ondragleave(DragEvent) - ondragover(DragEvent) - ondragstart(DragEvent) - ondrop(DragEvent) - onfocus(FocusEvent) - onkeydown(KeyboardEvent) - onkeypress(KeyboardEvent) - onkeyup(KeyboardEvent) - onloadstart(ProgressEvent) - onmousedown(MouseEvent) - onmouseenter(MouseEvent) - onmouseleave(MouseEvent) - onmouseout(MouseEvent) - onmouseover(MouseEvent) - onmouseup(MouseEvent) - onprogress(ProgressEvent) - onsubmit(FocusEvent) - onwheel(WheelEvent) - onanimationcancel(AnimationEvent) - onanimationend(AnimationEvent) - onanimationiteration(AnimationEvent) - onanimationstart(AnimationEvent) - ongotpointercapture(PointerEvent) - onloadend(ProgressEvent) - onlostpointercapture(PointerEvent) - onpointercancel(PointerEvent) - onpointerdown(PointerEvent) - onpointerenter(PointerEvent) - onpointerleave(PointerEvent) - onpointerout(PointerEvent) - onpointerover(PointerEvent) - onpointerup(PointerEvent) - ontouchcancel(TouchEvent) - ontouchend(TouchEvent) - ontransitioncancel(TransitionEvent) - ontransitionend(TransitionEvent) - ontransitionrun(TransitionEvent) - ontransitionstart(TransitionEvent) -} - -// Best used with passive listeners, if you handle each and every event globally -impl_short! { - onmousemove(MouseEvent, true) - onscroll(Event, true) - onpointermove(PointerEvent, true) - ontouchmove(TouchEvent, true) - ontouchstart(TouchEvent, true) -} - -// More specialized cases -impl_action! { - onchange(Event) -> ChangeData => onchange_handler - oninput(InputEvent) -> InputData => oninput_handler -} diff --git a/yew/src/html/listener/listener_websys.rs b/yew/src/html/listener/listener_websys.rs new file mode 100644 index 00000000000..199986704c1 --- /dev/null +++ b/yew/src/html/listener/listener_websys.rs @@ -0,0 +1,273 @@ +// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events + +use wasm_bindgen::JsCast; +use web_sys::{Element, Event, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement}; + +macro_rules! impl_action { + ($($action:ident($type:ident) -> $ret:path => $convert:path)*) => {$( + impl_action!($action($type, ::std::u8::MIN) -> $ret => $convert); + )*}; + ($($action:ident($type:ident, $flags:path) -> $ret:path => $convert:path)*) => {$( + /// An abstract implementation of a listener. + #[doc(hidden)] + pub mod $action { + use crate::callback::Callback; + use crate::virtual_dom::Listener; + + /// 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 } + } + } + + /// And event type which keeps the returned type. + pub type Event = $ret; + + impl Listener for Wrapper { + fn kind(&self) -> &'static str { + stringify!($action) + } + + fn handle(&self, event: web_sys::Event) { + self.callback.emit($convert(event)); + } + + fn flags(&self) -> u8 { + match &self.callback { + Callback::Callback{flags, ..} => *flags, + _ => $flags, + } + } + } + } + )*}; +} + +pub(crate) fn nop_convert(t: T) -> T { + t +} + +pub(crate) fn cast_event(e: web_sys::Event) -> T +where + T: wasm_bindgen::JsCast, +{ + e.dyn_into().unwrap() +} + +// Reduces repetition for common cases +macro_rules! impl_short { + ($($action:ident)*) => { + impl_action! { + $( + $action(Event) -> web_sys::Event => crate::html::listener::nop_convert + )* + } + }; + ($($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 + oncopy + oncut + onpaste + onpointerlockchange + onpointerlockerror + onselectionchange + onselectstart + onshow +} + +// Specialized event type +impl_short! { + onauxclick(MouseEvent) + onblur(FocusEvent) + onclick(MouseEvent) + oncontextmenu(MouseEvent) + ondblclick(MouseEvent) + ondrag(DragEvent) + ondragend(DragEvent) + ondragenter(DragEvent) + ondragexit(DragEvent) + ondragleave(DragEvent) + ondragover(DragEvent) + ondragstart(DragEvent) + ondrop(DragEvent) + onfocus(FocusEvent) + onkeydown(KeyboardEvent) + onkeypress(KeyboardEvent) + onkeyup(KeyboardEvent) + onloadstart(ProgressEvent) + onmousedown(MouseEvent) + onmouseenter(MouseEvent) + onmouseleave(MouseEvent) + onmouseout(MouseEvent) + onmouseover(MouseEvent) + onmouseup(MouseEvent) + onprogress(ProgressEvent) + onsubmit(FocusEvent) + onwheel(WheelEvent) + onanimationcancel(AnimationEvent) + onanimationend(AnimationEvent) + onanimationiteration(AnimationEvent) + onanimationstart(AnimationEvent) + ongotpointercapture(PointerEvent) + onloadend(ProgressEvent) + onlostpointercapture(PointerEvent) + onpointercancel(PointerEvent) + onpointerdown(PointerEvent) + onpointerenter(PointerEvent) + onpointerleave(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)*) => { + impl_action! { + $( + $action(Event, crate::callback::PASSIVE) -> web_sys::Event + => crate::html::listener::nop_convert + )* + } + }; + ($($action:ident($type:ident))*) => { + impl_action! { + $( + $action($type, crate::callback::PASSIVE) -> web_sys::$type + => crate::html::listener::cast_event + )* + } + }; +} + +// Best used with passive listeners, if you handle each and every event globally +impl_passive! { + onscroll +} +impl_passive! { + onmousemove(MouseEvent) + onpointermove(PointerEvent) + ontouchmove(TouchEvent) + ontouchstart(TouchEvent) +} + +// More specialized cases +impl_action! { + onchange(Event) -> crate::html::listener::ChangeData => crate::html::listener::onchange_handler + oninput(InputEvent) -> crate::html::listener::InputData + => crate::html::listener::oninput_handler +} + +/// Extract target as Element from event +fn extract_target(event: &web_sys::Event) -> Element { + event + .target() + .expect("no target on event") + .dyn_into() + .unwrap() +} + +pub(crate) fn oninput_handler(event: Event) -> super::InputData { + let this = extract_target(&event); + let event: web_sys::InputEvent = cast_event(event); + + // Normally only InputElement or TextAreaElement can have an oninput event listener. In + // practice though any element with `contenteditable=true` may generate such events, + // therefore here we fall back to just returning the text content of the node. + // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event. + let value = this + .dyn_ref() + .map(|input: &HtmlInputElement| input.value()) + .or_else(|| { + this.dyn_ref() + .map(|input: &HtmlTextAreaElement| input.value()) + }) + .or_else(|| this.text_content()) + .expect(concat!( + "only an InputElement or TextAreaElement or an element with contenteditable=true ", + "can have an oninput event listener" + )); + super::InputData { value, event } +} + +pub(crate) fn onchange_handler(event: Event) -> super::ChangeData { + use super::ChangeData; + + let this = extract_target(&event); + + match this.node_name().as_ref() { + "INPUT" => { + let input = this.dyn_into::().unwrap(); + if input + .get_attribute("type") + .map(|value| value.eq_ignore_ascii_case("file")) + .unwrap_or(false) + { + ChangeData::Files(input.files().unwrap()) + } else { + ChangeData::Value(input.value()) + } + } + "TEXTAREA" => ChangeData::Value(this.dyn_into::().unwrap().value()), + "SELECT" => ChangeData::Select(this.dyn_into::().unwrap()), + _ => { + panic!(concat!( + "only an InputElement, TextAreaElement or SelectElement ", + "can have an onchange event listener" + )); + } + } +} diff --git a/yew/src/html/listener/macros.rs b/yew/src/html/listener/macros.rs deleted file mode 100644 index ecaf648c237..00000000000 --- a/yew/src/html/listener/macros.rs +++ /dev/null @@ -1,111 +0,0 @@ -#[macro_use] -macro_rules! impl_action { - ($($action:ident($type:ident) -> $ret:path => $convert:expr)*) => {$( - impl_action!($action($type, false) -> $ret => $convert); - )*}; - ($($action:ident($type:ident, $passive:literal) -> $ret:path => $convert:expr)*) => {$( - /// An abstract implementation of a listener. - #[doc(hidden)] - pub mod $action { - use cfg_if::cfg_if; - use crate::callback::Callback; - #[allow(unused_imports)] - use crate::html::listener::*; - use crate::virtual_dom::Listener; - cfg_if! { - if #[cfg(feature = "std_web")] { - use stdweb::web::event::$type; - use stdweb::web::{Element, IEventTarget}; - } else if #[cfg(feature = "web_sys")] { - use web_sys::Element; - } - } - - /// 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 } - } - } - - /// And event type which keeps the returned type. - pub type Event = $ret; - - #[cfg(feature = "std_web")] - type Argument = stdweb::Reference; - #[cfg(feature = "web_sys")] - type Argument = web_sys::Event; - - impl Listener for Wrapper { - fn kind(&self) -> &'static str { - stringify!($action) - } - - fn attach(&self, body: &Element, handler: Box) { - #[cfg(feature = "std_web")] - body.add_event_listener(move |event: $type| handler(event.into())); - - #[cfg(feature = "web_sys")] - crate::html::listener::macros::attach( - &self.kind()[2..], - body, - handler, - self.passive(), - ); - } - - fn handle(&self, event: Argument) { - self.callback.emit($convert(event)); - } - - #[cfg(feature = "web_sys")] - fn passive(&self) -> bool { - match &self.callback { - Callback::CallbackWithOpts{passive, ..} => *passive, - _ => $passive, - } - } - - fn handle_bubbled(&self) -> bool { - match &self.callback { - Callback::CallbackWithOpts{handle_bubbled, ..} => *handle_bubbled, - _ => false, - } - } - } - } - )*}; -} - -// Moved out to reduce instruction bloat -#[cfg(feature = "web_sys")] -pub(crate) fn attach( - kind: &str, - body: &web_sys::Element, - handler: Box, - passive: bool, -) { - use wasm_bindgen::prelude::*; - use wasm_bindgen::JsCast; - - let cl = Closure::wrap(handler); - AsRef::::as_ref(body) - .add_event_listener_with_callback_and_add_event_listener_options( - &kind[2..], - cl.as_ref().unchecked_ref(), - &{ - let mut opts = web_sys::AddEventListenerOptions::new(); - opts.passive(passive); - opts - }, - ) - .map_err(|e| format!("could not register global listener: {:?}", e)) - .unwrap(); - cl.forget(); // Never drop the closure as this event handler is static -} diff --git a/yew/src/html/listener/mod.rs b/yew/src/html/listener/mod.rs index b888e52a805..cc7e45f3638 100644 --- a/yew/src/html/listener/mod.rs +++ b/yew/src/html/listener/mod.rs @@ -1,35 +1,21 @@ -#[macro_use] -pub(crate) mod macros; -mod registry; -pub(crate) use registry::*; - use cfg_if::cfg_if; -use cfg_match::cfg_match; cfg_if! { if #[cfg(feature = "std_web")] { mod listener_stdweb; + pub use listener_stdweb::*; - use stdweb::Reference as Event; - use stdweb::js; - use stdweb::web::html_element::{InputElement, SelectElement, TextAreaElement}; - use stdweb::web::{Element, FileList, IElement, INode}; + use stdweb::web::FileList; use stdweb::web::event::InputEvent; - use std::convert::{TryInto, TryFrom}; - - pub use listener_stdweb::*; + use stdweb::web::html_element::SelectElement; } else if #[cfg(feature = "web_sys")] { - mod listener_web_sys; + mod listener_websys; + pub use listener_websys::*; - use wasm_bindgen::JsCast; - use web_sys::{ - Element, FileList, HtmlInputElement as InputElement, HtmlSelectElement as SelectElement, - HtmlTextAreaElement as TextAreaElement, - InputEvent, - Event, - }; + mod registry; + pub(crate) use registry::*; - pub use listener_web_sys::*; + use web_sys::{FileList, InputEvent, HtmlSelectElement as SelectElement}; } } @@ -57,119 +43,8 @@ pub enum ChangeData { Value(String), /// SelectElement in case of ` +

{state.text.clone()}

+ + } + } else { + html! { +
+ Message::SetText(s), + _ => Message::NOP, + }) + oninput=link + .callback_with_flags(Self::flags(), |InputData { value, .. }| { + Message::SetText(value) + }) + /> +

{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] + #[cfg(not(feature = "listener_benchmarks"))] + fn oninput() { + test_input_listener(|| { + web_sys::InputEvent::new_with_event_init_dict( + "input", + &web_sys::InputEventInit::new().bubbles(true), + ) + .unwrap() + }) + } + + #[test] + #[cfg(not(feature = "listener_benchmarks"))] + fn onchange() { + test_input_listener(|| { + web_sys::Event::new_with_event_init_dict( + "change", + &web_sys::EventInit::new().bubbles(true), + ) + .unwrap() + }) + } // TODO: sync vs passive vs deferred benchmark } From a948b0a683066a8bf09fdb14b233c15b214a2bc8 Mon Sep 17 00:00:00 2001 From: bakape Date: Fri, 11 Sep 2020 21:57:27 +0300 Subject: [PATCH 19/37] yew/listener: optimize listener registration --- yew/src/html/listener/listener_stdweb.rs | 6 +- yew/src/html/listener/listener_web_sys.rs | 5 +- yew/src/html/listener/registry.rs | 41 ++++--- yew/src/virtual_dom/mod.rs | 127 +++++++++++++++++++++- 4 files changed, 153 insertions(+), 26 deletions(-) diff --git a/yew/src/html/listener/listener_stdweb.rs b/yew/src/html/listener/listener_stdweb.rs index 30bb343be09..79ce1adaccd 100644 --- a/yew/src/html/listener/listener_stdweb.rs +++ b/yew/src/html/listener/listener_stdweb.rs @@ -85,7 +85,7 @@ macro_rules! impl_action { pub mod $action { use crate::callback::Callback; use crate::html::listener::EventListener; - use crate::virtual_dom::Listener; + use crate::virtual_dom::{Listener, ListenerKind}; use stdweb::web::event::$type; use stdweb::web::{Element, IEventTarget}; @@ -106,8 +106,8 @@ macro_rules! impl_action { pub type Event = $ret; impl Listener for Wrapper { - fn kind(&self) -> &'static str { - stringify!($action) + fn kind(&self) -> ListenerKind { + ListenerKind::$action } fn attach(&self, element: &Element) -> EventListener { diff --git a/yew/src/html/listener/listener_web_sys.rs b/yew/src/html/listener/listener_web_sys.rs index 0fa929fdf70..35dcf02c8ad 100644 --- a/yew/src/html/listener/listener_web_sys.rs +++ b/yew/src/html/listener/listener_web_sys.rs @@ -13,6 +13,7 @@ macro_rules! impl_action { pub mod $action { use crate::callback::{Callback, Flags}; use crate::virtual_dom::Listener; + use crate::virtual_dom::ListenerKind; /// A wrapper for a callback which attaches event listeners to elements. #[derive(Clone, Debug)] @@ -31,8 +32,8 @@ macro_rules! impl_action { pub type Event = $ret; impl Listener for Wrapper { - fn kind(&self) -> &'static str { - stringify!($action) + fn kind(&self) -> ListenerKind { + ListenerKind::$action } fn handle(&self, event: web_sys::Event) { diff --git a/yew/src/html/listener/registry.rs b/yew/src/html/listener/registry.rs index f2bcbe4b0ba..2f5326cd7c7 100644 --- a/yew/src/html/listener/registry.rs +++ b/yew/src/html/listener/registry.rs @@ -1,7 +1,7 @@ use crate::{ callback::{Callback, Flags, DEFER, HANDLE_BUBBLED, PASSIVE}, services::render::{RenderService, RenderTask}, - virtual_dom::{Listener, Listeners}, + virtual_dom::{Listener, ListenerKind, Listeners}, }; use std::{ cell::RefCell, @@ -23,7 +23,7 @@ thread_local! { #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] struct EventDescriptor { - kind: &'static str, + kind: ListenerKind, flags: Flags, } @@ -40,7 +40,7 @@ impl From<&dyn Listener> for EventDescriptor { #[derive(Default, Debug)] struct Registry { /// Counter for assigning new IDs - id_counter: u64, + id_counter: u32, /// Events with registered handlers that are possibly passive handling: HashSet, @@ -49,7 +49,7 @@ struct Registry { bubbling: HashSet, /// Contains all registered event listeners by listener ID - by_id: HashMap>>, + by_id: HashMap>>, /// Event handling deferred until the next animation frame deferred: VecDeque<(EventDescriptor, Event, web_sys::Element)>, @@ -60,7 +60,7 @@ struct Registry { /// The registry is never dropped in production. #[cfg(test)] #[allow(clippy::type_complexity)] - registered: Vec<(&'static str, Closure)>, + registered: Vec<(ListenerKind, Closure)>, } impl Registry { @@ -70,7 +70,7 @@ impl Registry { } /// Register all passed listeners under ID - fn register(&mut self, id: u64, listeners: Vec>) { + fn register(&mut self, id: u32, listeners: Vec>) { let mut by_id = HashMap::with_capacity(listeners.len()); for l in listeners.into_iter() { // Create global listener, if not yet created @@ -83,7 +83,7 @@ impl Registry { as Box); AsRef::::as_ref(body) .add_event_listener_with_callback_and_add_event_listener_options( - &key.kind[2..], + &key.kind.as_ref()[2..], cl.as_ref().unchecked_ref(), &{ let mut opts = web_sys::AddEventListenerOptions::new(); @@ -100,7 +100,7 @@ impl Registry { #[cfg(not(test))] cl.forget(); #[cfg(test)] - self.registered.push((&key.kind[2..], cl)); + self.registered.push((key.kind, cl)); }); self.handling.insert(key); @@ -116,17 +116,17 @@ impl Registry { } /// Unregister any existing listeners for ID - fn unregister(&mut self, id: &u64) { + 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) -> u64 { + 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::JsString::from(id.to_string())).unwrap() { + if !js_sys::Reflect::set(el, &prop, &js_sys::Number::from(id)).unwrap() { panic!("failed to set listener ID property"); } }); @@ -182,10 +182,14 @@ impl Registry { .with(|prop| js_sys::Reflect::get(el, &prop).ok()) .map(|v| v.dyn_into().ok()) .flatten() - .map(|v: js_sys::JsString| String::from(v).parse().ok()) - .flatten() - .map(|id: u64| { - Registry::with(|r| r.by_id.get(&id).map(|s| s.get(&desc)).flatten().cloned()) + .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() { @@ -215,7 +219,10 @@ impl Drop for Registry { BODY.with(|body| { for (kind, cl) in std::mem::take(&mut self.registered) { AsRef::::as_ref(body) - .remove_event_listener_with_callback(kind, cl.as_ref().unchecked_ref()) + .remove_event_listener_with_callback( + &kind.as_ref()[2..], + cl.as_ref().unchecked_ref(), + ) .unwrap(); } }); @@ -262,7 +269,7 @@ pub(crate) fn remove_listeners(listeners: &Listeners) { } /// Compare passed listeners for equality with a registered set via pointer equality checks -pub(crate) fn compare_listeners(registered_id: u64, rhs: &[Rc]) -> bool { +pub(crate) fn compare_listeners(registered_id: u32, rhs: &[Rc]) -> bool { // Empty sets are not stored if rhs.is_empty() { return false; diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 93e1a7e3035..f456839f054 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -42,11 +42,130 @@ pub use self::vtag::VTag; #[doc(inline)] pub use self::vtext::VText; +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, std::hash::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 + 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 +} + /// 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; + fn kind(&self) -> ListenerKind; /// Attaches a listener to the Element #[cfg(feature = "std_web")] @@ -67,13 +186,13 @@ impl fmt::Debug for dyn Listener { feature = "web_sys" => write!( f, "Listener {{ kind: {}, flags: {:?} }}", - self.kind(), + self.kind().as_ref(), self.flags() ), feature = "stdweb" => write!( f, "Listener {{ kind: {} }}", - self.kind() + self.kind().as_ref() ), } } @@ -84,7 +203,7 @@ impl fmt::Debug for dyn Listener { pub enum Listeners { /// Added to global registry by ID #[cfg(feature = "web_sys")] - Registered(u64), + Registered(u32), /// Added to the Element. Stored so they are removed on VTag drop. #[cfg(feature = "std_web")] From 709de5124a57406776bc01d3861288ba63d3d20a Mon Sep 17 00:00:00 2001 From: bakape Date: Fri, 11 Sep 2020 23:05:44 +0300 Subject: [PATCH 20/37] yew/listener: remove benchmark placeholders Seems easybench-wasm does not support specifying a module path. --- yew/Cargo.toml | 1 - yew/src/html/listener/registry.rs | 15 +-------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/yew/Cargo.toml b/yew/Cargo.toml index b94a683a586..7864719cd82 100644 --- a/yew/Cargo.toml +++ b/yew/Cargo.toml @@ -146,7 +146,6 @@ yaml = ["serde_yaml"] msgpack = ["rmp-serde"] cbor = ["serde_cbor"] listener_tests = [] -listener_benchmarks = [] [package.metadata.docs.rs] features = ["yaml", "cbor", "toml", "msgpack", "doc_test"] diff --git a/yew/src/html/listener/registry.rs b/yew/src/html/listener/registry.rs index 2f5326cd7c7..177666394d0 100644 --- a/yew/src/html/listener/registry.rs +++ b/yew/src/html/listener/registry.rs @@ -295,11 +295,7 @@ pub(crate) fn compare_listeners(registered_id: u32, rhs: &[Rc]) -> }) } -#[cfg(all( - test, - feature = "wasm_test", - any(feature = "listener_tests", feature = "listener_benchmarks") -))] +#[cfg(all(test, feature = "wasm_test", feature = "listener_tests"))] mod tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); @@ -430,7 +426,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] fn synchronous() { struct Synchronous(); @@ -466,7 +461,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] async fn passive() { struct Passive(); @@ -501,7 +495,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] fn bubbling() { struct Bubbling(); @@ -551,7 +544,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] async fn deferred() { struct Deferred(); @@ -564,7 +556,6 @@ mod tests { assert_async::().await; } - #[cfg(not(feature = "listener_benchmarks"))] fn test_input_listener(make_event: impl Fn() -> E) where E: JsCast + std::fmt::Debug, @@ -630,7 +621,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] fn oninput() { test_input_listener(|| { web_sys::InputEvent::new_with_event_init_dict( @@ -642,7 +632,6 @@ mod tests { } #[test] - #[cfg(not(feature = "listener_benchmarks"))] fn onchange() { test_input_listener(|| { web_sys::Event::new_with_event_init_dict( @@ -652,6 +641,4 @@ mod tests { .unwrap() }) } - - // TODO: sync vs passive vs deferred benchmark } From 39fdb84d8a8281b2249291d0621868be6e4d4cfe Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 13 Sep 2020 17:10:36 +0300 Subject: [PATCH 21/37] yew/callback: revert CallbackOnce -> Once --- yew/src/callback.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/yew/src/callback.rs b/yew/src/callback.rs index 747133dbdab..9a68ccbbb66 100644 --- a/yew/src/callback.rs +++ b/yew/src/callback.rs @@ -81,10 +81,10 @@ pub enum Callback { /// A callback which can only be called once. The callback will panic if it is /// called more than once. - Once(Rc>), + CallbackOnce(Rc>), } -type Once = RefCell>>; +type CallbackOnce = RefCell>>; impl From for Callback { fn from(func: F) -> Self { @@ -102,7 +102,7 @@ impl Clone for Callback { cb: cb.clone(), flags: *flags, }, - Callback::Once(cb) => Callback::Once(cb.clone()), + Callback::CallbackOnce(cb) => Callback::CallbackOnce(cb.clone()), } } } @@ -111,7 +111,9 @@ impl Clone for Callback { impl PartialEq for Callback { fn eq(&self, other: &Callback) -> bool { match (&self, &other) { - (Callback::Once(cb), Callback::Once(other_cb)) => Rc::ptr_eq(cb, other_cb), + (Callback::CallbackOnce(cb), Callback::CallbackOnce(other_cb)) => { + Rc::ptr_eq(cb, other_cb) + } ( Callback::Callback { cb, flags }, Callback::Callback { @@ -128,7 +130,7 @@ impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let data = match self { Callback::Callback { .. } => "Callback<_>", - Callback::Once(_) => "Once<_>", + Callback::CallbackOnce(_) => "CallbackOnce<_>", }; f.write_str(data) @@ -140,7 +142,7 @@ impl Callback { pub fn emit(&self, value: IN) { match self { Callback::Callback { cb, .. } => cb(value), - Callback::Once(rc) => { + Callback::CallbackOnce(rc) => { let cb = rc.replace(None); let f = cb.expect("callback contains `FnOnce` which has already been used"); f(value) @@ -155,7 +157,7 @@ impl Callback { where F: FnOnce(IN) + 'static, { - Callback::Once(Rc::new(RefCell::new(Some(Box::new(func))))) + Callback::CallbackOnce(Rc::new(RefCell::new(Some(Box::new(func))))) } /// Creates a "no-op" callback which can be used when it is not suitable to use an From 47c0df998a3f97fa12a9aa219c3c1d7d94c2592a Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 13 Sep 2020 18:37:29 +0300 Subject: [PATCH 22/37] yew: convert listener_tests to a build flag --- ci/run_tests.sh | 8 +++----- yew/Cargo.toml | 1 - yew/src/html/listener/registry.rs | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ci/run_tests.sh b/ci/run_tests.sh index 6960ab6c60c..72de477be57 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -16,17 +16,15 @@ fi echo "running tests with flags: ${test_flags[*]} and features: ${test_features}" -# Event listener tests have to be run in their own `wasm-pack test` invocation or they won't work. -test_features_listeners="${test_features},listener_tests" - # TESTS set -x (cd yew && wasm-pack test "${test_flags[@]}" -- --features "${test_features}" && - wasm-pack test "${test_flags[@]}" -- --features "${test_features_listeners}" \ - yew::html::listener::registry && + # Event listener tests have to be run in their own `wasm-pack test` invocation or they won't work. + RUSTFLAGS="--cfg listener_tests" wasm-pack test "${test_flags[@]}" -- \ + --features "${test_features}" yew::html::listener::registry && cargo test --doc --features doc_test,wasm_test,yaml,msgpack,cbor,toml && cargo test --doc --features doc_test,wasm_test,yaml,msgpack,cbor,toml \ --features std_web,agent,services --no-default-features) diff --git a/yew/Cargo.toml b/yew/Cargo.toml index 7864719cd82..fa6bb1e4aab 100644 --- a/yew/Cargo.toml +++ b/yew/Cargo.toml @@ -145,7 +145,6 @@ agent = ["bincode"] yaml = ["serde_yaml"] msgpack = ["rmp-serde"] cbor = ["serde_cbor"] -listener_tests = [] [package.metadata.docs.rs] features = ["yaml", "cbor", "toml", "msgpack", "doc_test"] diff --git a/yew/src/html/listener/registry.rs b/yew/src/html/listener/registry.rs index 177666394d0..12de496e508 100644 --- a/yew/src/html/listener/registry.rs +++ b/yew/src/html/listener/registry.rs @@ -295,7 +295,7 @@ pub(crate) fn compare_listeners(registered_id: u32, rhs: &[Rc]) -> }) } -#[cfg(all(test, feature = "wasm_test", feature = "listener_tests"))] +#[cfg(all(test, feature = "wasm_test", listener_tests))] mod tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); From 56afcee5fc6efc65b24dea652524288baaa96a07 Mon Sep 17 00:00:00 2001 From: bakape Date: Mon, 14 Sep 2020 19:18:31 +0300 Subject: [PATCH 23/37] Apply suggestions from code review Co-authored-by: Simon --- yew/src/callback.rs | 2 +- yew/src/html/listener/registry.rs | 2 +- yew/src/virtual_dom/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yew/src/callback.rs b/yew/src/callback.rs index 9a68ccbbb66..cfc259f1ebc 100644 --- a/yew/src/callback.rs +++ b/yew/src/callback.rs @@ -5,7 +5,7 @@ use std::fmt; use std::rc::Rc; /// Flags that modify default callback behaviour -#[derive(Eq, PartialEq, Clone, Copy, std::hash::Hash, Debug)] +#[derive(Eq, PartialEq, Clone, Copy, Hash, Debug)] pub struct Flags(u8); impl std::ops::BitAnd for Flags { diff --git a/yew/src/html/listener/registry.rs b/yew/src/html/listener/registry.rs index 12de496e508..e8491c8021f 100644 --- a/yew/src/html/listener/registry.rs +++ b/yew/src/html/listener/registry.rs @@ -60,7 +60,7 @@ struct Registry { /// The registry is never dropped in production. #[cfg(test)] #[allow(clippy::type_complexity)] - registered: Vec<(ListenerKind, Closure)>, + registered: Vec<(ListenerKind, Closure)>, } impl Registry { diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 79ddb276f26..87ccc4af6a7 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -47,7 +47,7 @@ macro_rules! gen_listener_kinds { /// 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, std::hash::Hash, Debug)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[allow(non_camel_case_types)] #[allow(missing_docs)] pub enum ListenerKind { From 460ef4f776b34a4d896a71b47f58b64dd7c5d756 Mon Sep 17 00:00:00 2001 From: bakape Date: Fri, 18 Sep 2020 21:40:04 +0300 Subject: [PATCH 24/37] yew: fix doc comments --- yew/src/virtual_dom/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 87ccc4af6a7..06c6bc6a97f 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -175,7 +175,7 @@ pub trait Listener { #[cfg(feature = "web_sys")] fn handle(&self, event: Event); - /// Defines flags toi modify the handling of the event. See yew::callback for more details. + /// Defines flags to modify the handling of the event. See yew::callback for more details. #[cfg(feature = "web_sys")] fn flags(&self) -> crate::callback::Flags; } @@ -209,7 +209,7 @@ pub enum Listeners { #[cfg(feature = "std_web")] Registered(Rc>), - /// Not yet added to the element + /// Not yet added to the element or registry Pending(Vec>), } From 99a5139dd7e15be6f13e68f62012ad57cd3a6825 Mon Sep 17 00:00:00 2001 From: bakape Date: Fri, 18 Sep 2020 22:19:09 +0300 Subject: [PATCH 25/37] yew: simplify iteration --- yew/src/virtual_dom/mod.rs | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 06c6bc6a97f..08445e64331 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -247,23 +247,12 @@ impl PartialEq for Listeners { fn compare_listener_slices(lhs: &[Rc], rhs: &[Rc]) -> bool { if lhs.len() != rhs.len() { - return false; - } - - let mut lhs_it = lhs.iter(); - let mut rhs_it = rhs.iter(); - loop { - match (lhs_it.next(), rhs_it.next()) { - (Some(lhs), Some(rhs)) => - { - #[allow(clippy::vtable_address_comparisons)] - if !Rc::ptr_eq(lhs, rhs) { - return false; - } - } - (None, None) => return true, - _ => return false, - }; + false + } else { + lhs.iter().zip(rhs.iter()).all(|(lhs, rhs)| { + #[allow(clippy::vtable_address_comparisons)] + Rc::ptr_eq(lhs, rhs) + }) } } From eb5cff88db6d0e44ca31ce3b9e9cf3e818200456 Mon Sep 17 00:00:00 2001 From: bakape Date: Fri, 18 Sep 2020 22:50:07 +0300 Subject: [PATCH 26/37] yew/remove unneeded default passive listeners --- yew/src/html/listener/listener_web_sys.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/yew/src/html/listener/listener_web_sys.rs b/yew/src/html/listener/listener_web_sys.rs index 35dcf02c8ad..5b78376c5c9 100644 --- a/yew/src/html/listener/listener_web_sys.rs +++ b/yew/src/html/listener/listener_web_sys.rs @@ -99,6 +99,7 @@ impl_short! { onratechange onreset onresize + onscroll onsecuritypolicyviolation onseeked onseeking @@ -145,6 +146,7 @@ impl_short! { onmousedown(MouseEvent) onmouseenter(MouseEvent) onmouseleave(MouseEvent) + onmousemove(MouseEvent) onmouseout(MouseEvent) onmouseover(MouseEvent) onmouseup(MouseEvent) @@ -162,6 +164,7 @@ impl_short! { onpointerdown(PointerEvent) onpointerenter(PointerEvent) onpointerleave(PointerEvent) + onpointermove(PointerEvent) onpointerout(PointerEvent) onpointerover(PointerEvent) onpointerup(PointerEvent) @@ -174,14 +177,6 @@ impl_short! { } macro_rules! impl_passive { - ($($action:ident)*) => { - impl_action! { - $( - $action(Event, crate::callback::PASSIVE) -> web_sys::Event - => std::convert::identity - )* - } - }; ($($action:ident($type:ident))*) => { impl_action! { $( @@ -192,13 +187,8 @@ macro_rules! impl_passive { }; } -// Best used with passive listeners, if you handle each and every event globally +// Best used with passive listeners for responsiveness impl_passive! { - onscroll -} -impl_passive! { - onmousemove(MouseEvent) - onpointermove(PointerEvent) ontouchmove(TouchEvent) ontouchstart(TouchEvent) } From 4aaa3ec9887c4514273969c1d3c610c4bd855e74 Mon Sep 17 00:00:00 2001 From: bakape Date: Sat, 7 Aug 2021 23:50:02 +0300 Subject: [PATCH 27/37] yew/listeners: DRY some more --- Makefile.toml | 2 +- packages/yew/src/virtual_dom/listeners.rs | 147 +++++++++++----------- 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 7afe891e319..5c916bc57d7 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -81,7 +81,7 @@ private = true workspace = true dependencies = ["doc-test"] -# TODO: port event listners tets and make them a dep of pr-flow +# TODO: port event listener tets and make them a dep of pr-flow # # Event listener tests have to be run in their own `wasm-pack test` invocation or they won't work. # RUSTFLAGS="--cfg listener_tests" wasm-pack test "${test_flags[@]}" -- \ # --features "${test_features}" yew::html::listener::registry && diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index e9e1f979530..a94565c847d 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -325,18 +325,14 @@ impl From<&dyn Listener> for EventDescriptor { } } -/// Global multiplexing event handler registry +/// Ensures global event handler registration. +// +// Separate struct to DRY, while avoiding partial struct mutability. #[derive(Default, Debug)] -struct Registry { - /// Counter for assigning new IDs - id_counter: u32, - +struct GlobalHandlers { /// Events with registered handlers that are possibly passive handling: HashSet, - /// Contains all registered event listeners by listener ID - by_id: HashMap>>>, - /// Keep track of all listeners to drop them on registry drop. /// The registry is never dropped in production. #[cfg(test)] @@ -344,6 +340,72 @@ struct Registry { 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 fn with(f: impl FnOnce(&mut Registry) -> R) -> R { @@ -356,19 +418,7 @@ impl Registry { HashMap::>>::with_capacity(listeners.len()); for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { let desc = EventDescriptor::from(l.deref()); - - // Ensure a descriptor has a global event handler assigned - if !self.handling.contains(&desc) { - let cl = Self::create_global_handler(desc); - // 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); - } - + self.global.ensure_handled(desc); by_desc.entry(desc).or_default().push(l); } self.by_id.insert(id, by_desc); @@ -384,48 +434,12 @@ impl Registry { for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { let desc = EventDescriptor::from(l.deref()); - - // Ensure a descriptor has a global event handler assigned - if !self.handling.contains(&desc) { - let cl = Self::create_global_handler(desc); - // 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); - } - + self.global.ensure_handled(desc); by_desc.entry(desc).or_default().push(l); } } } - /// Create a global event handler to call into the registry - fn create_global_handler(desc: EventDescriptor) -> Closure { - 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 - }) - } - /// Unregister any existing listeners for ID fn unregister(&mut self, id: &u32) { self.by_id.remove(id); @@ -497,23 +511,6 @@ impl Registry { } } -// Enable resetting between tests -#[cfg(test)] -impl Drop for Registry { - 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(); - } - }); - } -} - #[cfg(all(test, feature = "wasm_test", listener_tests))] mod tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; From c2a167391266f6fb5856704bca0cbf47b31a6f2c Mon Sep 17 00:00:00 2001 From: bakape Date: Sat, 7 Aug 2021 23:57:12 +0300 Subject: [PATCH 28/37] yew/listener: fix clippy warnings --- packages/yew/src/virtual_dom/listeners.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index a94565c847d..feb7b8aa81f 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -252,7 +252,7 @@ impl PartialEq for Listeners { | (Pending(pending), Registered(registered_id)) => { use std::option::Option::None; - Registry::with(|reg| match reg.by_id.get(®istered_id) { + Registry::with(|reg| match reg.by_id.get(registered_id) { Some(reg) => { if reg.len() != pending.len() { return false; @@ -262,7 +262,7 @@ impl PartialEq for Listeners { match reg.get(&EventDescriptor::from(l.deref())) { Some(reg) => reg.iter().any(|reg| { #[allow(clippy::vtable_address_comparisons)] - Rc::ptr_eq(reg, &l) + Rc::ptr_eq(reg, l) }), None => false, } @@ -451,7 +451,7 @@ impl Registry { self.id_counter += 1; LISTENER_ID_PROP.with(|prop| { - if !js_sys::Reflect::set(el, &prop, &js_sys::Number::from(id)).unwrap() { + if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() { panic!("failed to set listener ID property"); } }); @@ -476,7 +476,7 @@ impl Registry { 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()) + .with(|prop| js_sys::Reflect::get(el, prop).ok()) .map(|v| v.dyn_into().ok()) .flatten() .map(|num: js_sys::Number| { From a4a31c2c7cc4d7aae0b4f66723d3fb3e17097fea Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 8 Aug 2021 00:16:29 +0300 Subject: [PATCH 29/37] yew/listeners: remove legacy comment --- packages/yew/src/virtual_dom/listeners.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index feb7b8aa81f..18c5db52872 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -44,7 +44,6 @@ pub trait Listener { /// Makes the event listener passive. /// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). - /// Defines flags to modify the handling of the event. See yew::callback for more details. fn passive(&self) -> bool; } From 814ee9404e13f0f815075dd7e3c6d48849f37d72 Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 8 Aug 2021 00:29:58 +0300 Subject: [PATCH 30/37] yew/listeners: document stopping propagation --- packages/yew/src/virtual_dom/listeners.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 18c5db52872..0af1b4a8bb6 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -26,6 +26,10 @@ static mut BUBBLE_EVENTS: bool = true; /// 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 { @@ -504,6 +508,9 @@ impl Registry { 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); } } From f1b1720612dd83b1f9012c7fd411f3b839324c28 Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 14:27:22 +0300 Subject: [PATCH 31/37] yew/listeners: update tests --- Makefile.toml | 5 -- packages/yew/src/html/component/lifecycle.rs | 2 +- packages/yew/src/virtual_dom/listeners.rs | 74 +++++++------------- 3 files changed, 25 insertions(+), 56 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 5c916bc57d7..898ef912a76 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -81,11 +81,6 @@ private = true workspace = true dependencies = ["doc-test"] -# TODO: port event listener tets and make them a dep of pr-flow -# # Event listener tests have to be run in their own `wasm-pack test` invocation or they won't work. -# RUSTFLAGS="--cfg listener_tests" wasm-pack test "${test_flags[@]}" -- \ -# --features "${test_features}" yew::html::listener::registry && - [tasks.doc-test] private = true command = "cargo" diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index f625e459db2..90b5394640d 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -234,8 +234,8 @@ mod tests { #[derive(Clone, Properties, Default)] struct Props { lifecycle: Rc>>, - #[allow(dead_code)] #[cfg(feature = "wasm_test")] + #[allow(dead_code)] create_message: Option, update_message: RefCell>, view_message: RefCell>, diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 0af1b4a8bb6..11c41bd7cd6 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -517,17 +517,16 @@ impl Registry { } } -#[cfg(all(test, feature = "wasm_test", listener_tests))] +#[cfg(all(test, feature = "wasm_test"))] mod tests { use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); use crate::{ - callback::{Flags, DEFER, HANDLE_BUBBLED, NO_FLAGS, PASSIVE}, html, - html::listener::{ChangeData, InputData}, + html::{ChangeData, InputData}, utils::document, - App, Component, ComponentLink, Html, + AppHandle, Component, ComponentLink, Html, }; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; @@ -548,7 +547,9 @@ mod tests { } trait Mixin { - fn flags() -> Flags; + fn passive() -> Option { + None + } fn view(link: &ComponentLink, state: &State) -> Html where @@ -560,7 +561,7 @@ mod tests { } } else { html! { - + {state.clicked} } @@ -630,7 +631,7 @@ mod tests { .unwrap() } - fn init(tag: &str) -> (ComponentLink>, web_sys::HtmlElement) + fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) where M: Mixin, { @@ -642,20 +643,16 @@ mod tests { let root = document().create_element("div").unwrap(); document().body().unwrap().append_child(&root).unwrap(); - let link = App::>::new().mount(root); + let app = crate::start_app_in_element::>(root); - (link, get_el_by_tag(tag)) + (app, get_el_by_tag(tag)) } #[test] fn synchronous() { struct Synchronous(); - impl Mixin for Synchronous { - fn flags() -> Flags { - NO_FLAGS - } - } + impl Mixin for Synchronous {} let (link, el) = init::("a"); @@ -687,8 +684,8 @@ mod tests { struct Passive(); impl Mixin for Passive { - fn flags() -> Flags { - PASSIVE + fn passive() -> Option { + Some(true) } } @@ -721,10 +718,6 @@ mod tests { struct Bubbling(); impl Mixin for Bubbling { - fn flags() -> Flags { - HANDLE_BUBBLED - } - fn view(link: &ComponentLink, state: &State) -> Html where C: Component, @@ -738,10 +731,10 @@ mod tests { } } else { - let cb = link.callback_with_flags(Self::flags(), |_| Message::Click); + let cb = link.callback(|_| Message::Click); html! { -
- + @@ -765,19 +758,6 @@ mod tests { assert_count(&el, 4); } - #[test] - async fn deferred() { - struct Deferred(); - - impl Mixin for Deferred { - fn flags() -> Flags { - DEFER - } - } - - assert_async::().await; - } - fn test_input_listener(make_event: impl Fn() -> E) where E: JsCast + std::fmt::Debug, @@ -785,10 +765,6 @@ mod tests { struct Input(); impl Mixin for Input { - fn flags() -> Flags { - NO_FLAGS - } - fn view(link: &ComponentLink, state: &State) -> Html where C: Component, @@ -805,16 +781,14 @@ mod tests {
Message::SetText(s), - _ => Message::NOP, - }) - oninput=link - .callback_with_flags(Self::flags(), |InputData { value, .. }| { - Message::SetText(value) - }) - /> + onchange={link.callback(|d: ChangeData| match d { + ChangeData::Value(s) => Message::SetText(s), + _ => Message::NOP, + })} + oninput={link.callback(|InputData { value, .. }| { + Message::SetText(value) + })} + />

{state.text.clone()}

} From 13f4f0c1921260b8dd5ba25e9b5b76ad23ea20ac Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 14:43:49 +0300 Subject: [PATCH 32/37] ci: see how test run on stable --- .github/workflows/pull-request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 59b0aa75cf2..25bbb8c5637 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -113,8 +113,9 @@ jobs: strategy: matrix: toolchain: - - 1.49.0 # MSRV + # Works for me on 1.54. Let's see what happens. - stable + - 1.49.0 # MSRV steps: - uses: actions/checkout@v2 From d1ea63910652875d2d182f3d25e1bf8ed24137ff Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 14:49:01 +0300 Subject: [PATCH 33/37] ci: let's find the new MSRV --- .github/workflows/pull-request.yml | 3 +-- packages/yew/src/html/component/lifecycle.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 25bbb8c5637..556f5c59d98 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -113,9 +113,8 @@ jobs: strategy: matrix: toolchain: - # Works for me on 1.54. Let's see what happens. + - 1.53.0 # MSRV - stable - - 1.49.0 # MSRV steps: - uses: actions/checkout@v2 diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 90b5394640d..f625e459db2 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -234,8 +234,8 @@ mod tests { #[derive(Clone, Properties, Default)] struct Props { lifecycle: Rc>>, - #[cfg(feature = "wasm_test")] #[allow(dead_code)] + #[cfg(feature = "wasm_test")] create_message: Option, update_message: RefCell>, view_message: RefCell>, From 9bd2219faf40b30ae27bfce2fa4db15b8cf24b60 Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 14:53:10 +0300 Subject: [PATCH 34/37] ci: try to run integration tests only on stable --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 556f5c59d98..2ce36a99026 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -113,7 +113,7 @@ jobs: strategy: matrix: toolchain: - - 1.53.0 # MSRV + # - 1.49.0 # MSRV - stable steps: From cd5a3bc01a260fe00e281a4735c6abc66af03d7b Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 15:35:14 +0300 Subject: [PATCH 35/37] yew/test: clean up residual dirty state --- .github/workflows/pull-request.yml | 2 +- packages/yew/Cargo.toml | 1 + packages/yew/src/virtual_dom/vtag.rs | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ce36a99026..59b0aa75cf2 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -113,7 +113,7 @@ jobs: strategy: matrix: toolchain: - # - 1.49.0 # MSRV + - 1.49.0 # MSRV - stable steps: diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index a81c0d86edb..2318eb59c9a 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -14,6 +14,7 @@ readme = "../README.md" keywords = ["web", "webasm", "javascript"] categories = ["gui", "wasm", "web-programming"] description = "A framework for making client-side single-page apps" +rust = "1.49" [dependencies] anyhow = "1" diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index d3edc1f57c6..4d8e4df9f2e 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1028,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] From 26172a2f7f847d032a57203a7fd7926ebe9d032b Mon Sep 17 00:00:00 2001 From: bakape Date: Sun, 15 Aug 2021 15:48:04 +0300 Subject: [PATCH 36/37] yew/listeners: minor doc string and inline fixes --- packages/yew/src/virtual_dom/listeners.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 11c41bd7cd6..15395453da5 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -37,7 +37,7 @@ pub fn set_event_bubbling(bubble: bool) { } } -/// The `Listener` trait is an universal implementation of an event listener +/// 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 @@ -46,7 +46,7 @@ pub trait Listener { /// Handles an event firing fn handle(&self, event: web_sys::Event); - /// Makes the event listener passive. + /// Makes the event listener passive. See /// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). fn passive(&self) -> bool; } @@ -411,6 +411,7 @@ struct Registry { 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())) } From 939e97bec50fe66cab052d8c1bead6970db30ff2 Mon Sep 17 00:00:00 2001 From: bakape Date: Sat, 21 Aug 2021 20:31:47 +0300 Subject: [PATCH 37/37] yew/listener: document reasonning for function --- packages/yew/src/html/listener/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/yew/src/html/listener/mod.rs b/packages/yew/src/html/listener/mod.rs index 832cd475534..c3ba572d667 100644 --- a/packages/yew/src/html/listener/mod.rs +++ b/packages/yew/src/html/listener/mod.rs @@ -47,6 +47,12 @@ fn extract_target(event: &Event) -> Element { .unwrap() } +/// 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: wasm_bindgen::JsCast,