Skip to content

Commit

Permalink
Support passing callbacks to elements (yewstack#777)
Browse files Browse the repository at this point in the history
* Support passing callbacks to elements

* Fix tests
  • Loading branch information
jstarry authored and llebout committed Jan 20, 2020
1 parent 6030b05 commit fd1ebd6
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 107 deletions.
4 changes: 2 additions & 2 deletions crates/macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
});

Expand Down Expand Up @@ -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)
}});
Expand Down
13 changes: 12 additions & 1 deletion crates/macro/src/html_tree/html_tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) }
Expand Down Expand Up @@ -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)*
Expand Down
35 changes: 14 additions & 21 deletions crates/macro/src/html_tree/html_tag/tag_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::{Expr, ExprClosure, ExprTuple, Ident, Pat};

pub struct TagAttributes {
pub attributes: Vec<TagAttribute>,
pub listeners: Vec<TokenStream>,
pub listeners: Vec<(Ident, TokenStream)>,
pub classes: Option<ClassesForm>,
pub value: Option<Expr>,
pub kind: Option<Expr>,
Expand Down Expand Up @@ -120,14 +120,14 @@ impl TagAttributes {
}
}

fn map_listener(listener: TagListener) -> ParseResult<TokenStream> {
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,
Expand All @@ -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))
}
}

Expand Down
3 changes: 1 addition & 2 deletions examples/nested_list/src/list.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
43 changes: 17 additions & 26 deletions src/html/listener.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::*;
use crate::callback::Callback;
use crate::virtual_dom::Listener;
use stdweb::web::html_element::SelectElement;
#[allow(unused_imports)]
Expand All @@ -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<F>(Option<F>);

/// 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<Event>,
}

impl<F, MSG> From<F> for Wrapper<F>
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<Event>) -> Self {
Wrapper { callback }
}
}

impl<T, COMP> Listener<COMP> for Wrapper<T>
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<COMP>)
-> 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)
}
Expand Down
2 changes: 1 addition & 1 deletion src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
3 changes: 3 additions & 0 deletions src/html/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub(crate) enum ComponentUpdate<COMP: Component> {
Properties(COMP::Properties),
}

/// A reference to the parent's scope which will be used later to send messages.
pub type ScopeHolder<PARENT> = Rc<RefCell<Option<Scope<PARENT>>>>;

/// A context which allows sending messages to a component.
pub struct Scope<COMP: Component> {
shared_state: Shared<ComponentState<COMP>>,
Expand Down
19 changes: 12 additions & 7 deletions src/virtual_dom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<COMP: Component> {
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<COMP>) -> EventListenerHandle;
/// Attaches a listener to the element.
fn attach(&self, element: &Element) -> EventListenerHandle;
}

impl<COMP: Component> fmt::Debug for dyn Listener<COMP> {
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<COMP> = Vec<Box<dyn Listener<COMP>>>;
type Listeners = Vec<Box<dyn Listener>>;

/// A map of attributes.
type Attributes = HashMap<String, String>;
Expand Down Expand Up @@ -201,3 +200,9 @@ pub trait VDiff {
parent_scope: &Scope<Self::Component>,
) -> Option<Node>;
}

/// Transforms properties and attaches a parent scope holder to callbacks for sending messages.
pub trait Transformer<PARENT: Component, FROM, TO> {
/// Transforms one type to another.
fn transform(scope_holder: ScopeHolder<PARENT>, from: FROM) -> TO;
}
23 changes: 3 additions & 20 deletions src/virtual_dom/vcomp.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<PARENT> = Rc<RefCell<Option<Scope<PARENT>>>>;

/// A virtual component.
pub struct VComp<PARENT: Component> {
type_id: TypeId,
Expand Down Expand Up @@ -131,12 +128,6 @@ impl<PARENT: Component> VComp<PARENT> {
}
}

/// Transforms properties and attaches a parent scope holder to callbacks for sending messages.
pub trait Transformer<PARENT: Component, FROM, TO> {
/// Transforms one type to another.
fn transform(scope_holder: ScopeHolder<PARENT>, from: FROM) -> TO;
}

impl<PARENT, T> Transformer<PARENT, T, T> for VComp<PARENT>
where
PARENT: Component,
Expand Down Expand Up @@ -189,15 +180,7 @@ where
F: Fn(IN) -> PARENT::Message + 'static,
{
fn transform(scope: ScopeHolder<PARENT>, from: F) -> Option<Callback<IN>> {
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::<PARENT>::transform(scope, from))
}
}

Expand Down
Loading

0 comments on commit fd1ebd6

Please sign in to comment.