From fd1ebd65c538090bd4e4a0fb4a92168c6ba078f1 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sat, 7 Dec 2019 13:42:19 -0500 Subject: [PATCH] Support passing callbacks to elements (#777) * Support passing callbacks to elements * Fix tests --- crates/macro/src/html_tree/html_component.rs | 4 +- crates/macro/src/html_tree/html_tag/mod.rs | 13 ++- .../src/html_tree/html_tag/tag_attributes.rs | 35 +++---- examples/nested_list/src/list.rs | 3 +- src/html/listener.rs | 43 ++++----- src/html/mod.rs | 2 +- src/html/scope.rs | 3 + src/virtual_dom/mod.rs | 19 ++-- src/virtual_dom/vcomp.rs | 23 +---- src/virtual_dom/vtag.rs | 96 +++++++++++++++---- tests/macro/html-tag-fail.stderr | 15 +-- tests/macro/html-tag-pass.rs | 3 + 12 files changed, 152 insertions(+), 107 deletions(-) diff --git a/crates/macro/src/html_tree/html_component.rs b/crates/macro/src/html_tree/html_component.rs index 398f8ad6b0b..723782400c4 100644 --- a/crates/macro/src/html_tree/html_component.rs +++ b/crates/macro/src/html_tree/html_component.rs @@ -140,7 +140,7 @@ impl ToTokens for HtmlComponent { Props::List(ListProps { props, .. }) => { let set_props = props.iter().map(|HtmlProp { label, value }| { quote_spanned! { value.span()=> - .#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value)) + .#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value)) } }); @@ -181,7 +181,7 @@ impl ToTokens for HtmlComponent { #validate_props } - let #vcomp_scope: ::yew::virtual_dom::vcomp::ScopeHolder<_> = ::std::default::Default::default(); + let #vcomp_scope: ::yew::html::ScopeHolder<_> = ::std::default::Default::default(); let __yew_node_ref: ::yew::html::NodeRef = #node_ref; ::yew::virtual_dom::VChild::<#ty, _>::new(#init_props, #vcomp_scope, __yew_node_ref) }}); diff --git a/crates/macro/src/html_tree/html_tag/mod.rs b/crates/macro/src/html_tree/html_tag/mod.rs index 59da189b9b6..98a9aa053f4 100644 --- a/crates/macro/src/html_tree/html_tag/mod.rs +++ b/crates/macro/src/html_tree/html_tag/mod.rs @@ -102,6 +102,7 @@ impl ToTokens for HtmlTag { } = &attributes; let vtag = Ident::new("__yew_vtag", tag_name.span()); + let vtag_scope = Ident::new("__yew_vtag_scope", Span::call_site()); let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| { let label_str = label.to_string(); quote_spanned! {value.span() => (#label_str.to_owned(), (#value).to_string()) } @@ -148,9 +149,19 @@ impl ToTokens for HtmlTag { #vtag.node_ref = #node_ref; } }); + let listeners = listeners.iter().map(|(name, callback)| { + quote_spanned! {name.span()=> { + ::yew::html::#name::Wrapper::new( + <::yew::virtual_dom::vtag::VTag<_> as ::yew::virtual_dom::Transformer<_, _, _>>::transform( + #vtag_scope.clone(), #callback + ) + ) + }} + }); tokens.extend(quote! {{ - let mut #vtag = ::yew::virtual_dom::vtag::VTag::new(#name); + let #vtag_scope: ::yew::html::ScopeHolder<_> = ::std::default::Default::default(); + let mut #vtag = ::yew::virtual_dom::vtag::VTag::new_with_scope(#name, #vtag_scope.clone()); #(#set_kind)* #(#set_value)* #(#add_href)* diff --git a/crates/macro/src/html_tree/html_tag/tag_attributes.rs b/crates/macro/src/html_tree/html_tag/tag_attributes.rs index b56d6c874c9..5a6dbb3e933 100644 --- a/crates/macro/src/html_tree/html_tag/tag_attributes.rs +++ b/crates/macro/src/html_tree/html_tag/tag_attributes.rs @@ -9,7 +9,7 @@ use syn::{Expr, ExprClosure, ExprTuple, Ident, Pat}; pub struct TagAttributes { pub attributes: Vec, - pub listeners: Vec, + pub listeners: Vec<(Ident, TokenStream)>, pub classes: Option, pub value: Option, pub kind: Option, @@ -120,14 +120,14 @@ impl TagAttributes { } } - fn map_listener(listener: TagListener) -> ParseResult { + fn map_listener(listener: TagListener) -> ParseResult<(Ident, TokenStream)> { let TagListener { name, event_name, handler, } = listener; - match handler { + let callback: TokenStream = match handler { Expr::Closure(closure) => { let ExprClosure { inputs, @@ -150,29 +150,22 @@ impl TagAttributes { Pat::Wild(pat) => Ok(pat.into_token_stream()), _ => Err(syn::Error::new_spanned(or_span, "invalid closure argument")), }?; - let handler = - Ident::new(&format!("__yew_{}_handler", name.to_string()), name.span()); - let listener = - Ident::new(&format!("__yew_{}_listener", name.to_string()), name.span()); + let callback = + Ident::new(&format!("__yew_{}_callback", name.to_string()), name.span()); let segment = syn::PathSegment { ident: Ident::new(&event_name, name.span()), arguments: syn::PathArguments::None, }; - let var_type = quote! { ::yew::events::#segment }; - let wrapper_type = quote! { ::yew::html::#name::Wrapper }; - let listener_stream = quote_spanned! {name.span()=> { - let #handler = move | #var: #var_type | #body; - let #listener = #wrapper_type::from(#handler); - #listener - }}; - - Ok(listener_stream) + + quote_spanned! {name.span()=> { + let #callback = move | #var: ::yew::events::#segment | #body; + #callback + }} } - _ => Err(syn::Error::new_spanned( - &name, - format!("`{}` attribute value should be a closure", name), - )), - } + callback => callback.into_token_stream(), + }; + + Ok((name, callback)) } } diff --git a/examples/nested_list/src/list.rs b/examples/nested_list/src/list.rs index 3b370ef195a..835404880a6 100644 --- a/examples/nested_list/src/list.rs +++ b/examples/nested_list/src/list.rs @@ -1,9 +1,8 @@ use crate::{header::Props as HeaderProps, ListHeader}; use crate::{item::Props as ItemProps, ListItem}; use std::fmt; -use yew::html::{ChildrenRenderer, NodeRef}; +use yew::html::{ChildrenRenderer, NodeRef, ScopeHolder}; use yew::prelude::*; -use yew::virtual_dom::vcomp::ScopeHolder; use yew::virtual_dom::{VChild, VComp, VNode}; #[derive(Debug)] diff --git a/src/html/listener.rs b/src/html/listener.rs index 31a3d6be5f0..99d51de5c13 100644 --- a/src/html/listener.rs +++ b/src/html/listener.rs @@ -1,4 +1,4 @@ -use super::*; +use crate::callback::Callback; use crate::virtual_dom::Listener; use stdweb::web::html_element::SelectElement; #[allow(unused_imports)] @@ -14,42 +14,33 @@ macro_rules! impl_action { use stdweb::web::event::{IEvent, $type}; use super::*; - /// A wrapper for a callback. - /// Listener extracted from here when attached. - #[allow(missing_debug_implementations)] - pub struct Wrapper(Option); - - /// And event type which keeps the returned type. - pub type Event = $ret; + /// A wrapper for a callback which attaches event listeners to elements. + #[derive(Clone, Debug)] + pub struct Wrapper { + callback: Callback, + } - impl From for Wrapper - where - MSG: 'static, - F: Fn($ret) -> MSG + 'static, - { - fn from(handler: F) -> Self { - Wrapper(Some(handler)) + impl Wrapper { + /// Create a wrapper for an event-typed callback + pub fn new(callback: Callback) -> Self { + Wrapper { callback } } } - impl Listener for Wrapper - where - T: Fn($ret) -> COMP::Message + 'static, - COMP: Component, - { + /// 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(&mut self, element: &Element, mut activator: Scope) - -> EventListenerHandle { - let handler = self.0.take().expect("tried to attach listener twice"); + fn attach(&self, element: &Element) -> EventListenerHandle { let this = element.clone(); + let callback = self.callback.clone(); let listener = move |event: $type| { event.stop_propagation(); - let handy_event: $ret = $convert(&this, event); - let msg = handler(handy_event); - activator.send_message(msg); + callback.emit($convert(&this, event)); }; element.add_event_listener(listener) } diff --git a/src/html/mod.rs b/src/html/mod.rs index 91d7c9e6ad7..507cfa4af63 100644 --- a/src/html/mod.rs +++ b/src/html/mod.rs @@ -7,8 +7,8 @@ mod listener; mod scope; pub use listener::*; -pub use scope::Scope; pub(crate) use scope::{ComponentUpdate, HiddenScope}; +pub use scope::{Scope, ScopeHolder}; use crate::callback::Callback; use crate::virtual_dom::{VChild, VList, VNode}; diff --git a/src/html/scope.rs b/src/html/scope.rs index 0eeedeb889a..f59adb87e44 100644 --- a/src/html/scope.rs +++ b/src/html/scope.rs @@ -16,6 +16,9 @@ pub(crate) enum ComponentUpdate { Properties(COMP::Properties), } +/// A reference to the parent's scope which will be used later to send messages. +pub type ScopeHolder = Rc>>>; + /// A context which allows sending messages to a component. pub struct Scope { shared_state: Shared>, diff --git a/src/virtual_dom/mod.rs b/src/virtual_dom/mod.rs index bb31fd3ed45..1a6caece3fe 100644 --- a/src/virtual_dom/mod.rs +++ b/src/virtual_dom/mod.rs @@ -16,26 +16,25 @@ pub use self::vlist::VList; pub use self::vnode::VNode; pub use self::vtag::VTag; pub use self::vtext::VText; -use crate::html::{Component, Scope}; +use crate::html::{Component, Scope, ScopeHolder}; /// `Listener` trait is an universal implementation of an event listener /// which helps to bind Rust-listener to JS-listener (DOM). -pub trait Listener { +pub trait Listener { /// Returns standard name of DOM's event. fn kind(&self) -> &'static str; - /// Attaches listener to the element and uses scope instance to send - /// prepared event back to the yew main loop. - fn attach(&mut self, element: &Element, scope: Scope) -> EventListenerHandle; + /// Attaches a listener to the element. + fn attach(&self, element: &Element) -> EventListenerHandle; } -impl fmt::Debug for dyn Listener { +impl fmt::Debug for dyn Listener { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Listener {{ kind: {} }}", self.kind()) } } /// A list of event listeners. -type Listeners = Vec>>; +type Listeners = Vec>; /// A map of attributes. type Attributes = HashMap; @@ -201,3 +200,9 @@ pub trait VDiff { parent_scope: &Scope, ) -> Option; } + +/// Transforms properties and attaches a parent scope holder to callbacks for sending messages. +pub trait Transformer { + /// Transforms one type to another. + fn transform(scope_holder: ScopeHolder, from: FROM) -> TO; +} diff --git a/src/virtual_dom/vcomp.rs b/src/virtual_dom/vcomp.rs index 3a4ec80a000..dfffd42b3d8 100644 --- a/src/virtual_dom/vcomp.rs +++ b/src/virtual_dom/vcomp.rs @@ -1,8 +1,8 @@ //! This module contains the implementation of a virtual component `VComp`. -use super::{VDiff, VNode}; +use super::{Transformer, VDiff, VNode}; use crate::callback::Callback; -use crate::html::{Component, ComponentUpdate, HiddenScope, NodeRef, Scope}; +use crate::html::{Component, ComponentUpdate, HiddenScope, NodeRef, Scope, ScopeHolder}; use std::any::TypeId; use std::cell::RefCell; use std::fmt; @@ -18,9 +18,6 @@ enum GeneratorType { Overwrite(HiddenScope), } -/// A reference to the parent's scope which will be used later to send messages. -pub type ScopeHolder = Rc>>>; - /// A virtual component. pub struct VComp { type_id: TypeId, @@ -131,12 +128,6 @@ impl VComp { } } -/// Transforms properties and attaches a parent scope holder to callbacks for sending messages. -pub trait Transformer { - /// Transforms one type to another. - fn transform(scope_holder: ScopeHolder, from: FROM) -> TO; -} - impl Transformer for VComp where PARENT: Component, @@ -189,15 +180,7 @@ where F: Fn(IN) -> PARENT::Message + 'static, { fn transform(scope: ScopeHolder, from: F) -> Option> { - let callback = move |arg| { - let msg = from(arg); - if let Some(ref mut sender) = *scope.borrow_mut() { - sender.send_message(msg); - } else { - panic!("Parent component hasn't activated this callback yet"); - } - }; - Some(callback.into()) + Some(VComp::::transform(scope, from)) } } diff --git a/src/virtual_dom/vtag.rs b/src/virtual_dom/vtag.rs index 93f073569da..1e4ef4b1d75 100644 --- a/src/virtual_dom/vtag.rs +++ b/src/virtual_dom/vtag.rs @@ -1,7 +1,10 @@ //! This module contains the implementation of a virtual element node `VTag`. -use super::{Attributes, Classes, Listener, Listeners, Patch, Reform, VDiff, VList, VNode}; -use crate::html::{Component, NodeRef, Scope}; +use super::{ + Attributes, Classes, Listener, Listeners, Patch, Reform, Transformer, VDiff, VList, VNode, +}; +use crate::callback::Callback; +use crate::html::{Component, NodeRef, Scope, ScopeHolder}; use log::warn; use std::borrow::Cow; use std::cmp::PartialEq; @@ -22,17 +25,17 @@ pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; /// A type for a virtual /// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) /// representation. -pub struct VTag { +pub struct VTag { /// A tag of the element. tag: Cow<'static, str>, /// A reference to the `Element`. pub reference: Option, /// List of attached listeners. - pub listeners: Listeners, + pub listeners: Listeners, /// List of attributes. pub attributes: Attributes, /// List of children nodes - pub children: VList, + pub children: VList, /// List of attached classes. pub classes: Classes, /// Contains a value of an @@ -50,14 +53,24 @@ pub struct VTag { pub checked: bool, /// A node reference used for DOM access in Component lifecycle methods pub node_ref: NodeRef, - /// _Service field_. Keeps handler for attached listeners - /// to have an opportunity to drop them later. + /// Keeps handler for attached listeners to have an opportunity to drop them later. captured: Vec, + /// Holds a reference to the parent component scope for callback activation. + scope_holder: ScopeHolder, } -impl VTag { +impl VTag { /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM). pub fn new>>(tag: S) -> Self { + Self::new_with_scope(tag, ScopeHolder::default()) + } + + /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM) and parent + /// scope holder for callback activation. + pub fn new_with_scope>>( + tag: S, + scope_holder: ScopeHolder, + ) -> Self { VTag { tag: tag.into(), reference: None, @@ -72,6 +85,7 @@ impl VTag { // In HTML node `checked` attribute sets `defaultChecked` parameter, // but we use own field to control real `checked` parameter checked: false, + scope_holder, } } @@ -81,12 +95,12 @@ impl VTag { } /// Add `VNode` child. - pub fn add_child(&mut self, child: VNode) { + pub fn add_child(&mut self, child: VNode) { self.children.add_child(child); } /// Add multiple `VNode` children. - pub fn add_children(&mut self, children: Vec>) { + pub fn add_children(&mut self, children: Vec>) { for child in children { self.add_child(child); } @@ -159,15 +173,15 @@ impl VTag { /// Adds new listener to the node. /// It's boxed because we want to keep it in a single list. - /// Lates `Listener::attach` called to attach actual listener to a DOM node. - pub fn add_listener(&mut self, listener: Box>) { + /// Later `Listener::attach` will attach an actual listener to a DOM node. + pub fn add_listener(&mut self, listener: Box) { self.listeners.push(listener); } /// Adds new listeners to the node. /// They are boxed because we want to keep them in a single list. - /// Lates `Listener::attach` called to attach actual listener to a DOM node. - pub fn add_listeners(&mut self, listeners: Vec>>) { + /// 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); } @@ -346,8 +360,8 @@ impl VTag { } } -impl VDiff for VTag { - type Component = COMP; +impl VDiff for VTag { + type Component = PARENT; /// Remove VTag from parent. fn detach(&mut self, parent: &Element) -> Option { @@ -454,11 +468,14 @@ impl VDiff for VTag { let element = self.reference.clone().expect("element expected"); - for mut listener in self.listeners.drain(..) { - let handle = listener.attach(&element, parent_scope.clone()); + for listener in self.listeners.drain(..) { + let handle = listener.attach(&element); self.captured.push(handle); } + // Activate scope + *self.scope_holder.borrow_mut() = Some(parent_scope.clone()); + // Process children self.children.apply( &element, @@ -473,7 +490,7 @@ impl VDiff for VTag { } } -impl fmt::Debug for VTag { +impl fmt::Debug for VTag { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "VTag {{ tag: {} }}", self.tag) } @@ -495,8 +512,8 @@ fn set_checked(input: &InputElement, value: bool) { js!( @(no_return) @{input}.checked = @{value}; ); } -impl PartialEq for VTag { - fn eq(&self, other: &VTag) -> bool { +impl PartialEq for VTag { + fn eq(&self, other: &VTag) -> bool { self.tag == other.tag && self.value == other.value && self.kind == other.kind @@ -521,3 +538,40 @@ pub(crate) fn not(option: &Option) -> &Option<()> { &Some(()) } } + +impl Transformer for VTag +where + PARENT: Component, +{ + fn transform(_: ScopeHolder, from: T) -> T { + from + } +} + +impl<'a, PARENT, T> Transformer for VTag +where + PARENT: Component, + T: Clone, +{ + fn transform(_: ScopeHolder, from: &'a T) -> T { + from.clone() + } +} + +impl<'a, PARENT, F, IN> Transformer> for VTag +where + PARENT: Component, + F: Fn(IN) -> PARENT::Message + 'static, +{ + fn transform(scope: ScopeHolder, from: F) -> Callback { + let callback = move |arg| { + let msg = from(arg); + if let Some(ref mut sender) = *scope.borrow_mut() { + sender.send_message(msg); + } else { + panic!("Parent component hasn't activated this callback yet"); + } + }; + callback.into() + } +} diff --git a/tests/macro/html-tag-fail.stderr b/tests/macro/html-tag-fail.stderr index 7ff76430ad4..147ec3cb66d 100644 --- a/tests/macro/html-tag-fail.stderr +++ b/tests/macro/html-tag-fail.stderr @@ -102,12 +102,6 @@ error: only one `class` attribute allowed 23 | html! {
}; | ^^^^^ -error: `onclick` attribute value should be a closure - --> $DIR/html-tag-fail.rs:32:20 - | -32 | html! { }; - | ^^^^^^^ - error: there must be one closure argument --> $DIR/html-tag-fail.rs:33:28 | @@ -190,6 +184,15 @@ error[E0277]: the trait bound `yew::html::Href: std::convert::From<()>` is not s > = note: required because of the requirements on the impl of `std::convert::Into` for `()` +error[E0308]: mismatched types + --> $DIR/html-tag-fail.rs:32:20 + | +32 | html! { }; + | ^^^^^^^ expected struct `yew::callback::Callback`, found integer + | + = note: expected type `yew::callback::Callback` + found type `{integer}` + error[E0599]: no method named `to_string` found for type `NotToString` in the current scope --> $DIR/html-tag-fail.rs:37:27 | diff --git a/tests/macro/html-tag-pass.rs b/tests/macro/html-tag-pass.rs index 122cae3c733..f660b5f96f9 100644 --- a/tests/macro/html-tag-pass.rs +++ b/tests/macro/html-tag-pass.rs @@ -4,6 +4,7 @@ mod helpers; pass_helper! { + let onclick = Callback::from(|_: ClickEvent| ()); let parent_ref = NodeRef::default(); html! {
@@ -37,6 +38,8 @@ pass_helper! {