From d795995e2039b1a34b8c1818dac2b1952bae9cf0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 11 Jun 2024 03:47:07 +0200 Subject: [PATCH] Create closure type; allow async event handlers in props; allow short hand event handlers (#2437) * create closure type; allow async event handlers in props; allow shorthand event handlers * test forwarding event handlers with the shorthand syntax * fix clippy * fix imports in spawn async doctest --- examples/calculator_mutable.rs | 2 +- examples/rsx_usage.rs | 2 +- examples/shorthand.rs | 10 +- examples/weather_app.rs | 4 +- packages/core-macro/src/props/mod.rs | 34 ++-- packages/core-macro/tests/event_handler.rs | 65 +++++++ packages/core/src/events.rs | 193 ++++++++++++++++----- packages/core/src/lib.rs | 18 +- packages/html/src/events/mod.rs | 53 +++--- packages/router/examples/simple_routes.rs | 2 +- packages/rsx/src/attribute.rs | 14 +- 11 files changed, 294 insertions(+), 103 deletions(-) create mode 100644 packages/core-macro/tests/event_handler.rs diff --git a/examples/calculator_mutable.rs b/examples/calculator_mutable.rs index f77c53e44..fc755c1f9 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -89,7 +89,7 @@ fn app() -> Element { #[component] fn CalculatorKey(name: String, onclick: EventHandler, children: Element) -> Element { rsx! { - button { class: "calculator-key {name}", onclick: move |e| onclick.call(e), {&children} } + button { class: "calculator-key {name}", onclick, {children} } } } diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 5bf10af69..9f3dd9750 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -21,7 +21,7 @@ //! //! ### Events //! - Handle events with the "onXYZ" syntax -//! - Closures can capture their environment with the 'a lifetime +//! - Closures can capture their environment with the 'static lifetime //! //! //! ### Components diff --git a/examples/shorthand.rs b/examples/shorthand.rs index f726c8a21..995dac5e1 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -27,12 +27,18 @@ fn app() -> Element { } #[component] -fn Component(a: i32, b: i32, c: i32, children: Element, onclick: EventHandler) -> Element { +fn Component( + a: i32, + b: i32, + c: i32, + children: Element, + onclick: EventHandler, +) -> Element { rsx! { div { "{a}" } div { "{b}" } div { "{c}" } div { {children} } - div { onclick: move |_| onclick.call(()) } + div { onclick } } } diff --git a/examples/weather_app.rs b/examples/weather_app.rs index a5774a327..64854a1b6 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -25,7 +25,7 @@ fn app() -> Element { div { class: "flex items-start justify-center flex-row", SearchBox { country } div { class: "flex flex-wrap w-full px-2", - div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full bg-white dark:bg-gray-600", + div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full dark:bg-gray-600", div { class: "px-6 py-6 relative", if let Some(Ok(weather)) = current_weather.read().as_ref() { CountryData { @@ -122,7 +122,7 @@ fn SearchBox(mut country: Signal) -> Element { placeholder: "Country name", "type": "text", autofocus: true, - oninput: move |e| input.set(e.value()) + oninput: move |e: FormEvent| input.set(e.value()) } svg { class: "w-4 h-4 absolute left-2.5 top-3.5", diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 4a9c93dc4..ad66b2da9 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -520,7 +520,7 @@ mod struct_info { empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single, modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple, }; - use super::{child_owned_type, looks_like_event_handler_type, looks_like_signal_type}; + use super::{child_owned_type, looks_like_callback_type, looks_like_signal_type}; #[derive(Debug)] pub struct StructInfo<'a> { @@ -634,12 +634,12 @@ mod struct_info { let event_handlers_fields: Vec<_> = self .included_fields() - .filter(|f| looks_like_event_handler_type(f.ty)) + .filter(|f| looks_like_callback_type(f.ty)) .collect(); let regular_fields: Vec<_> = self .included_fields() - .filter(|f| !looks_like_signal_type(f.ty) && !looks_like_event_handler_type(f.ty)) + .filter(|f| !looks_like_signal_type(f.ty) && !looks_like_callback_type(f.ty)) .map(|f| { let name = f.name; quote!(#name) @@ -1593,7 +1593,7 @@ Finally, call `.build()` to create the instance of `{name}`. } /// A helper function for paring types with a single generic argument. -fn extract_base_type_without_single_generic(ty: &Type) -> Option { +fn extract_base_type_without_generics(ty: &Type) -> Option { let Type::Path(ty) = ty else { return None; }; @@ -1670,11 +1670,11 @@ fn remove_option_wrapper(type_: Type) -> Type { /// Check if a type should be owned by the child component after conversion fn child_owned_type(ty: &Type) -> bool { - looks_like_signal_type(ty) || looks_like_event_handler_type(ty) + looks_like_signal_type(ty) || looks_like_callback_type(ty) } fn looks_like_signal_type(ty: &Type) -> bool { - match extract_base_type_without_single_generic(ty) { + match extract_base_type_without_generics(ty) { Some(path_without_generics) => { path_without_generics == parse_quote!(dioxus_core::prelude::ReadOnlySignal) || path_without_generics == parse_quote!(prelude::ReadOnlySignal) @@ -1684,13 +1684,16 @@ fn looks_like_signal_type(ty: &Type) -> bool { } } -fn looks_like_event_handler_type(ty: &Type) -> bool { +fn looks_like_callback_type(ty: &Type) -> bool { let type_without_option = remove_option_wrapper(ty.clone()); - match extract_base_type_without_single_generic(&type_without_option) { + match extract_base_type_without_generics(&type_without_option) { Some(path_without_generics) => { path_without_generics == parse_quote!(dioxus_core::prelude::EventHandler) || path_without_generics == parse_quote!(prelude::EventHandler) || path_without_generics == parse_quote!(EventHandler) + || path_without_generics == parse_quote!(dioxus_core::prelude::Callback) + || path_without_generics == parse_quote!(prelude::Callback) + || path_without_generics == parse_quote!(Callback) } None => false, } @@ -1709,20 +1712,21 @@ fn test_looks_like_type() { ReadOnlySignal, UnsyncStorage> ))); - assert!(looks_like_event_handler_type(&parse_quote!( + assert!(looks_like_callback_type(&parse_quote!( Option ))); - assert!(looks_like_event_handler_type(&parse_quote!( + assert!(looks_like_callback_type(&parse_quote!( std::option::Option> ))); - assert!(looks_like_event_handler_type(&parse_quote!( + assert!(looks_like_callback_type(&parse_quote!( Option> ))); - assert!(looks_like_event_handler_type(&parse_quote!( - EventHandler - ))); - assert!(looks_like_event_handler_type(&parse_quote!(EventHandler))); + assert!(looks_like_callback_type(&parse_quote!(EventHandler))); + assert!(looks_like_callback_type(&parse_quote!(EventHandler))); + + assert!(looks_like_callback_type(&parse_quote!(Callback))); + assert!(looks_like_callback_type(&parse_quote!(Callback))); } #[test] diff --git a/packages/core-macro/tests/event_handler.rs b/packages/core-macro/tests/event_handler.rs new file mode 100644 index 000000000..2563bb14e --- /dev/null +++ b/packages/core-macro/tests/event_handler.rs @@ -0,0 +1,65 @@ +use dioxus::prelude::*; + +// This test just checks that event handlers compile without explicit type annotations +// It will not actually run any code +#[test] +#[allow(unused)] +fn event_handlers_compile() { + fn app() -> Element { + let mut todos = use_signal(String::new); + rsx! { + input { + // Normal event handlers work without explicit type annotations + oninput: move |evt| todos.set(evt.value()), + } + button { + // async event handlers work without explicit type annotations + onclick: |event| async move { + println!("{event:?}"); + }, + } + + // New! You can now use async closures for custom event handlers! + // This shouldn't require an explicit type annotation + TakesEventHandler { onclick: |event| async move { + println!("{event:?}"); + } } + // Or you can accept a callback that returns a value + // This shouldn't require an explicit type annotation + TakesEventHandlerWithArg { double: move |value| (value * 2) as i32 } + } + } + + #[component] + fn TakesEventHandler(onclick: EventHandler) -> Element { + rsx! { + button { + // You can pass in EventHandlers directly to events + onclick: onclick, + "Click!" + } + button { + // Or use the shorthand syntax + onclick, + "Click!" + } + + // You should also be able to forward event handlers to other components with the shorthand syntax + TakesEventHandler { + onclick + } + } + } + + #[component] + fn TakesEventHandlerWithArg(double: Callback) -> Element { + let mut count = use_signal(|| 2); + rsx! { + button { + // Callbacks let you easily inject custom logic into your components + onclick: move |_| count.set(double(count()) as u32), + "{count}" + } + } + } +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index e0dfbcfd1..270351123 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,7 +1,8 @@ -use crate::{global_context::current_scope_id, Runtime, ScopeId}; +use crate::{global_context::current_scope_id, properties::SuperFrom, Runtime, ScopeId}; use generational_box::GenerationalBox; use std::{ cell::{Cell, RefCell}, + marker::PhantomData, rc::Rc, }; @@ -147,7 +148,6 @@ impl std::fmt::Debug for Event { /// /// This makes it possible to pass `move |evt| {}` style closures into components as property fields. /// -/// /// # Example /// /// ```rust, no_run @@ -169,7 +169,37 @@ impl std::fmt::Debug for Event { /// } /// } /// ``` -pub struct EventHandler { +pub type EventHandler = Callback; + +/// The callback type generated by the `rsx!` macro when an `on` field is specified for components. +/// +/// This makes it possible to pass `move |evt| {}` style closures into components as property fields. +/// +/// +/// # Example +/// +/// ```rust, ignore +/// rsx!{ +/// MyComponent { onclick: move |evt| { +/// tracing::debug!("clicked"); +/// 42 +/// } } +/// } +/// +/// #[derive(Props)] +/// struct MyProps { +/// onclick: Callback, +/// } +/// +/// fn MyComponent(cx: MyProps) -> Element { +/// rsx!{ +/// button { +/// onclick: move |evt| println!("number: {}", cx.onclick.call(evt)), +/// } +/// } +/// } +/// ``` +pub struct Callback { pub(crate) origin: ScopeId, /// During diffing components with EventHandler, we move the EventHandler over in place instead of rerunning the child component. /// @@ -190,87 +220,172 @@ pub struct EventHandler { /// /// We double box here because we want the data to be copy (GenerationalBox) and still update in place (ExternalListenerCallback) /// This isn't an ideal solution for performance, but it is non-breaking and fixes the issues described in - pub(super) callback: GenerationalBox>>, + pub(super) callback: GenerationalBox>>, } -impl std::fmt::Debug for EventHandler { +impl std::fmt::Debug for Callback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EventHandler") + f.debug_struct("Callback") .field("origin", &self.origin) .field("callback", &self.callback) .finish() } } -impl Default for EventHandler { +impl Default for Callback { fn default() -> Self { - EventHandler::new(|_| {}) + Callback::new(|_| Ret::default()) } } -impl From for EventHandler { - fn from(f: F) -> Self { - EventHandler::new(f) +/// A helper trait for [`Callback`]s that allows functions to accept a [`Callback`] that may return an async block which will automatically be spawned. +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// fn accepts_fn, Marker>(callback: impl FnMut(u32) -> Ret + 'static) { +/// let callback = Callback::new(callback); +/// } +/// // You can accept both async and non-async functions +/// accepts_fn(|x| async move { println!("{}", x) }); +/// accepts_fn(|x| println!("{}", x)); +/// ``` +#[rustversion::attr( + since(1.78.0), + diagnostic::on_unimplemented( + message = "`SpawnIfAsync` is not implemented for `{Self}`", + label = "Return Value", + note = "Closures (or event handlers) in dioxus need to return either: nothing (the unit type `()`), or an async block that dioxus will automatically spawn", + note = "You likely need to add a semicolon to the end of the event handler to make it return nothing", + ) +)] +pub trait SpawnIfAsync: Sized { + /// Spawn the value into the dioxus runtime if it is an async block + fn spawn(self) -> Ret; +} + +// Support for FnMut -> Ret for any return type +impl SpawnIfAsync<(), Ret> for Ret { + fn spawn(self) -> Ret { + self } } -impl Copy for EventHandler {} +// Support for FnMut -> async { anything } for the unit return type +#[doc(hidden)] +pub struct AsyncMarker(PhantomData); +impl + 'static, O> SpawnIfAsync, ()> for F { + fn spawn(self) { + crate::prelude::spawn(async move { + self.await; + }); + } +} -impl Clone for EventHandler { +// We can't directly forward the marker because it would overlap with a bunch of other impls, so we wrap it in another type instead +#[doc(hidden)] +pub struct MarkerWrapper(PhantomData); + +// Closure can be created from FnMut -> async { anything } or FnMut -> Ret +impl< + Function: FnMut(Args) -> Spawn + 'static, + Args: 'static, + Spawn: SpawnIfAsync + 'static, + Ret: 'static, + Marker, + > SuperFrom> for Callback +{ + fn super_from(input: Function) -> Self { + Callback::new(input) + } +} + +#[test] +fn closure_types_infer() { + #[allow(unused)] + fn compile_checks() { + // You should be able to use a closure as a callback + let callback: Callback<(), ()> = Callback::new(|_| {}); + // Or an async closure + let callback: Callback<(), ()> = Callback::new(|_| async {}); + + // You can also pass in a closure that returns a value + let callback: Callback<(), u32> = Callback::new(|_| 123); + + // Or pass in a value + let callback: Callback = Callback::new(|value: u32| async move { + println!("{}", value); + }); + } +} + +impl Copy for Callback {} + +impl Clone for Callback { fn clone(&self) -> Self { *self } } -impl PartialEq for EventHandler { +impl PartialEq for Callback { fn eq(&self, _: &Self) -> bool { true } } -type ExternalListenerCallback = Rc>; +type ExternalListenerCallback = Rc Ret>>; -impl EventHandler { - /// Create a new [`EventHandler`] from an [`FnMut`]. The callback is owned by the current scope and will be dropped when the scope is dropped. +impl Callback { + /// Create a new [`Callback`] from an [`FnMut`]. The callback is owned by the current scope and will be dropped when the scope is dropped. /// This should not be called directly in the body of a component because it will not be dropped until the component is dropped. #[track_caller] - pub fn new(mut f: impl FnMut(T) + 'static) -> EventHandler { + pub fn new, Marker>( + mut f: impl FnMut(Args) -> MaybeAsync + 'static, + ) -> Self { let owner = crate::innerlude::current_owner::(); - let callback = owner.insert(Some(Rc::new(RefCell::new(move |event: T| { - f(event); - })) as Rc>)); - EventHandler { + let callback = owner.insert(Some( + Rc::new(RefCell::new(move |event: Args| f(event).spawn())) + as Rc Ret>>, + )); + Self { callback, origin: current_scope_id().expect("to be in a dioxus runtime"), } } - /// Leak a new [`EventHandler`] that will not be dropped unless it is manually dropped. + /// Leak a new [`Callback`] that will not be dropped unless it is manually dropped. #[track_caller] - pub fn leak(mut f: impl FnMut(T) + 'static) -> EventHandler { - let callback = GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: T| { - f(event); - })) as Rc>)); - EventHandler { + pub fn leak(mut f: impl FnMut(Args) -> Ret + 'static) -> Self { + let callback = + GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: Args| f(event))) + as Rc Ret>>)); + Self { callback, origin: current_scope_id().expect("to be in a dioxus runtime"), } } - /// Call this event handler with the appropriate event type + /// Call this callback with the appropriate argument type /// - /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. - pub fn call(&self, event: T) { + /// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic. + pub fn call(&self, arguments: Args) -> Ret { if let Some(callback) = self.callback.read().as_ref() { Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin)); - { + let value = { let mut callback = callback.borrow_mut(); - callback(event); - } + callback(arguments) + }; Runtime::with(|rt| rt.scope_stack.borrow_mut().pop()); + value + } else { + panic!("Callback was manually dropped") } } + /// Create a `impl FnMut + Copy` closure from the Closure type + pub fn into_closure(self) -> impl FnMut(Args) -> Ret + Copy + 'static { + move |args| self.call(args) + } + /// Forcibly drop the internal handler callback, releasing memory /// /// This will force any future calls to "call" to not doing anything @@ -280,22 +395,22 @@ impl EventHandler { #[doc(hidden)] /// This should only be used by the `rsx!` macro. - pub fn __set(&mut self, value: ExternalListenerCallback) { + pub fn __set(&mut self, value: ExternalListenerCallback) { self.callback.set(Some(value)); } #[doc(hidden)] /// This should only be used by the `rsx!` macro. - pub fn __take(&self) -> ExternalListenerCallback { + pub fn __take(&self) -> ExternalListenerCallback { self.callback .read() .clone() - .expect("EventHandler was manually dropped") + .expect("Callback was manually dropped") } } -impl std::ops::Deref for EventHandler { - type Target = dyn Fn(T) + 'static; +impl std::ops::Deref for Callback { + type Target = dyn Fn(Args) -> Ret + 'static; fn deref(&self) -> &Self::Target { // https://github.com/dtolnay/case-studies/tree/master/callable-types diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 422c2026d..cfdf0eb08 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -60,10 +60,10 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering, AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode, - Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, Mutation, Mutations, - NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, Task, Template, - TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, VText, - VirtualDom, WriteMutations, + Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation, + Mutations, NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, SpawnIfAsync, + Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, + VText, VirtualDom, WriteMutations, }; /// The purpose of this module is to alleviate imports of many common types @@ -76,10 +76,10 @@ pub mod prelude { provide_context, provide_root_context, queue_effect, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary, use_hook, - use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Component, - ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, - IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, - ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, - Throw, VNode, VNodeInner, VirtualDom, + use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Callback, + Component, ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, + HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, + Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template, + TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom, }; } diff --git a/packages/html/src/events/mod.rs b/packages/html/src/events/mod.rs index fb9e1836a..c1bc613e5 100644 --- a/packages/html/src/events/mod.rs +++ b/packages/html/src/events/mod.rs @@ -25,16 +25,35 @@ macro_rules! impl_event { #[doc(alias = $js_name)] )? #[inline] - pub fn $name, T>(mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'static) -> ::dioxus_core::Attribute { + pub fn $name<__Marker>(mut _f: impl ::dioxus_core::prelude::SuperInto<::dioxus_core::prelude::EventHandler<::dioxus_core::Event<$data>>, __Marker>) -> ::dioxus_core::Attribute { + let event_handler = _f.super_into(); ::dioxus_core::Attribute::new( impl_event!(@name $name $($js_name)?), -::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event| { - _f(e.map(|e|e.into())).spawn(); + ::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event| { + event_handler.call(e.map(|e| e.into())); }), None, false, ).into() } + + #[doc(hidden)] + $( #[$attr] )* + pub mod $name { + use super::*; + + // When expanding the macro, we use this version of the function if we see an inline closure to give better type inference + $( #[$attr] )* + pub fn call_with_explicit_closure< + __Marker, + Return: ::dioxus_core::SpawnIfAsync<__Marker> + 'static, + >( + event_handler: impl FnMut(::dioxus_core::Event<$data>) -> Return + 'static, + ) -> ::dioxus_core::Attribute { + #[allow(deprecated)] + super::$name(event_handler) + } + } )* }; @@ -367,31 +386,3 @@ pub fn event_bubbles(evt: &str) -> bool { } } } - -#[doc(hidden)] -#[rustversion::attr( - since(1.78.0), - diagnostic::on_unimplemented( - message = "`EventHandlerReturn` is not implemented for `{Self}`", - label = "Return Value", - note = "Event handlers in dioxus need to return either: nothing (the unit type `()`), or an async block that dioxus will automatically spawn", - note = "You likely need to add a semicolon to the end of the event handler to make it return nothing", - ) -)] -pub trait EventReturn

: Sized { - fn spawn(self) {} -} - -impl EventReturn<()> for () {} -#[doc(hidden)] -pub struct AsyncMarker; - -impl EventReturn for T -where - T: std::future::Future + 'static, -{ - #[inline] - fn spawn(self) { - dioxus_core::prelude::spawn(self); - } -} diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index cb120deda..23dbda61d 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -119,7 +119,7 @@ fn Route3(dynamic: String) -> Element { rsx! { input { - oninput: move |evt| { + oninput: move |evt: FormEvent| { *current_route_str.write() = evt.value(); }, value: "{current_route_str}" diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 7d4af0303..548c1e4b0 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -4,7 +4,7 @@ use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, quote_spanned}; -use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr}; +use syn::{parse_quote, spanned::Spanned, Expr, ExprClosure, ExprIf, Ident, LitStr}; #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum AttributeType { @@ -246,8 +246,18 @@ impl ToTokens for ElementAttrNamed { } ElementAttrValue::EventTokens(tokens) => match &self.attr.name { ElementAttrName::BuiltIn(name) => { + let event_tokens_is_closure = + syn::parse2::(tokens.to_token_stream()).is_ok(); + let function_name = + quote_spanned! { tokens.span() => dioxus_elements::events::#name }; + let function = if event_tokens_is_closure { + // If we see an explicit closure, we can call the `call_with_explicit_closure` version of the event for better type inference + quote_spanned! { tokens.span() => #function_name::call_with_explicit_closure } + } else { + function_name + }; quote_spanned! { tokens.span() => - dioxus_elements::events::#name(#tokens) + #function(#tokens) } } ElementAttrName::Custom(_) => unreachable!("Handled elsewhere in the macro"),