Explorar o código

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
Evan Almloff hai 1 ano
pai
achega
d795995e20

+ 1 - 1
examples/calculator_mutable.rs

@@ -89,7 +89,7 @@ fn app() -> Element {
 #[component]
 fn CalculatorKey(name: String, onclick: EventHandler<MouseEvent>, children: Element) -> Element {
     rsx! {
-        button { class: "calculator-key {name}", onclick: move |e| onclick.call(e), {&children} }
+        button { class: "calculator-key {name}", onclick, {children} }
     }
 }
 

+ 1 - 1
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

+ 8 - 2
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<MouseEvent>,
+) -> Element {
     rsx! {
         div { "{a}" }
         div { "{b}" }
         div { "{c}" }
         div { {children} }
-        div { onclick: move |_| onclick.call(()) }
+        div { onclick }
     }
 }

+ 2 - 2
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<WeatherLocation>) -> 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",

+ 19 - 15
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<syn::Path> {
+fn extract_base_type_without_generics(ty: &Type) -> Option<syn::Path> {
     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<Option<i32>, UnsyncStorage>
     )));
 
-    assert!(looks_like_event_handler_type(&parse_quote!(
+    assert!(looks_like_callback_type(&parse_quote!(
         Option<EventHandler>
     )));
-    assert!(looks_like_event_handler_type(&parse_quote!(
+    assert!(looks_like_callback_type(&parse_quote!(
         std::option::Option<EventHandler<i32>>
     )));
-    assert!(looks_like_event_handler_type(&parse_quote!(
+    assert!(looks_like_callback_type(&parse_quote!(
         Option<EventHandler<MouseEvent>>
     )));
 
-    assert!(looks_like_event_handler_type(&parse_quote!(
-        EventHandler<i32>
-    )));
-    assert!(looks_like_event_handler_type(&parse_quote!(EventHandler)));
+    assert!(looks_like_callback_type(&parse_quote!(EventHandler<i32>)));
+    assert!(looks_like_callback_type(&parse_quote!(EventHandler)));
+
+    assert!(looks_like_callback_type(&parse_quote!(Callback<i32>)));
+    assert!(looks_like_callback_type(&parse_quote!(Callback<i32, u32>)));
 }
 
 #[test]

+ 65 - 0
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<MouseEvent>) -> 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<u32, i32>) -> 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}"
+            }
+        }
+    }
+}

+ 154 - 39
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<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
 ///
 /// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
 ///
-///
 /// # Example
 ///
 /// ```rust, no_run
@@ -169,7 +169,37 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
 ///     }
 /// }
 /// ```
-pub struct EventHandler<T = ()> {
+pub type EventHandler<T = ()> = Callback<T>;
+
+/// 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<MouseEvent, i32>,
+/// }
+///
+/// fn MyComponent(cx: MyProps) -> Element {
+///     rsx!{
+///         button {
+///             onclick: move |evt| println!("number: {}", cx.onclick.call(evt)),
+///         }
+///     }
+/// }
+/// ```
+pub struct Callback<Args = (), Ret = ()> {
     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<T = ()> {
     ///
     /// 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 <https://github.com/DioxusLabs/dioxus/pull/2298>
-    pub(super) callback: GenerationalBox<Option<ExternalListenerCallback<T>>>,
+    pub(super) callback: GenerationalBox<Option<ExternalListenerCallback<Args, Ret>>>,
 }
 
-impl<T> std::fmt::Debug for EventHandler<T> {
+impl<Args, Ret> std::fmt::Debug for Callback<Args, Ret> {
     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<T: 'static> Default for EventHandler<T> {
+impl<T: 'static, Ret: Default + 'static> Default for Callback<T, Ret> {
     fn default() -> Self {
-        EventHandler::new(|_| {})
+        Callback::new(|_| Ret::default())
     }
 }
 
-impl<F: FnMut(T) + 'static, T: 'static> From<F> for EventHandler<T> {
-    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<Ret: dioxus_core::SpawnIfAsync<Marker>, 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<Marker, Ret = ()>: 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<Ret> SpawnIfAsync<(), Ret> for Ret {
+    fn spawn(self) -> Ret {
+        self
+    }
+}
+
+// Support for FnMut -> async { anything } for the unit return type
+#[doc(hidden)]
+pub struct AsyncMarker<O>(PhantomData<O>);
+impl<F: std::future::Future<Output = O> + 'static, O> SpawnIfAsync<AsyncMarker<O>, ()> for F {
+    fn spawn(self) {
+        crate::prelude::spawn(async move {
+            self.await;
+        });
+    }
+}
+
+// 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<T>(PhantomData<T>);
+
+// Closure can be created from FnMut -> async { anything } or FnMut -> Ret
+impl<
+        Function: FnMut(Args) -> Spawn + 'static,
+        Args: 'static,
+        Spawn: SpawnIfAsync<Marker, Ret> + 'static,
+        Ret: 'static,
+        Marker,
+    > SuperFrom<Function, MarkerWrapper<Marker>> for Callback<Args, Ret>
+{
+    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<u32, ()> = Callback::new(|value: u32| async move {
+            println!("{}", value);
+        });
     }
 }
 
-impl<T> Copy for EventHandler<T> {}
+impl<Args, Ret> Copy for Callback<Args, Ret> {}
 
-impl<T> Clone for EventHandler<T> {
+impl<Args, Ret> Clone for Callback<Args, Ret> {
     fn clone(&self) -> Self {
         *self
     }
 }
 
-impl<T: 'static> PartialEq for EventHandler<T> {
+impl<Args: 'static, Ret: 'static> PartialEq for Callback<Args, Ret> {
     fn eq(&self, _: &Self) -> bool {
         true
     }
 }
 
-type ExternalListenerCallback<T> = Rc<RefCell<dyn FnMut(T)>>;
+type ExternalListenerCallback<Args, Ret> = Rc<RefCell<dyn FnMut(Args) -> Ret>>;
 
-impl<T: 'static> EventHandler<T> {
-    /// 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<Args: 'static, Ret: 'static> Callback<Args, Ret> {
+    /// 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<T> {
+    pub fn new<MaybeAsync: SpawnIfAsync<Marker, Ret>, Marker>(
+        mut f: impl FnMut(Args) -> MaybeAsync + 'static,
+    ) -> Self {
         let owner = crate::innerlude::current_owner::<generational_box::UnsyncStorage>();
-        let callback = owner.insert(Some(Rc::new(RefCell::new(move |event: T| {
-            f(event);
-        })) as Rc<RefCell<dyn FnMut(T)>>));
-        EventHandler {
+        let callback = owner.insert(Some(
+            Rc::new(RefCell::new(move |event: Args| f(event).spawn()))
+                as Rc<RefCell<dyn FnMut(Args) -> 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<T> {
-        let callback = GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: T| {
-            f(event);
-        })) as Rc<RefCell<dyn FnMut(T)>>));
-        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<RefCell<dyn FnMut(Args) -> 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<T: 'static> EventHandler<T> {
 
     #[doc(hidden)]
     /// This should only be used by the `rsx!` macro.
-    pub fn __set(&mut self, value: ExternalListenerCallback<T>) {
+    pub fn __set(&mut self, value: ExternalListenerCallback<Args, Ret>) {
         self.callback.set(Some(value));
     }
 
     #[doc(hidden)]
     /// This should only be used by the `rsx!` macro.
-    pub fn __take(&self) -> ExternalListenerCallback<T> {
+    pub fn __take(&self) -> ExternalListenerCallback<Args, Ret> {
         self.callback
             .read()
             .clone()
-            .expect("EventHandler was manually dropped")
+            .expect("Callback was manually dropped")
     }
 }
 
-impl<T: 'static> std::ops::Deref for EventHandler<T> {
-    type Target = dyn Fn(T) + 'static;
+impl<Args: 'static, Ret: 'static> std::ops::Deref for Callback<Args, Ret> {
+    type Target = dyn Fn(Args) -> Ret + 'static;
 
     fn deref(&self) -> &Self::Target {
         // https://github.com/dtolnay/case-studies/tree/master/callable-types

+ 9 - 9
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,
     };
 }

+ 22 - 31
packages/html/src/events/mod.rs

@@ -25,16 +25,35 @@ macro_rules! impl_event {
                 #[doc(alias = $js_name)]
             )?
             #[inline]
-            pub fn $name<E: crate::EventReturn<T>, 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<crate::PlatformEventData>| {
-                        _f(e.map(|e|e.into())).spawn();
+                    ::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
+                        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<P>: Sized {
-    fn spawn(self) {}
-}
-
-impl EventReturn<()> for () {}
-#[doc(hidden)]
-pub struct AsyncMarker;
-
-impl<T> EventReturn<AsyncMarker> for T
-where
-    T: std::future::Future<Output = ()> + 'static,
-{
-    #[inline]
-    fn spawn(self) {
-        dioxus_core::prelude::spawn(self);
-    }
-}

+ 1 - 1
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}"

+ 12 - 2
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::<ExprClosure>(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"),