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
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-06-10 18:47:07 -07:00
1 parent 79e18c2d62
commit d795995e20
11 files changed
+294 -103

No files matched your search

+154 -39
View File
@@ -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
}
}
impl<T> Copy for EventHandler<T> {}
// 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;
});
}
}
impl<T> Clone for EventHandler<T> {
// 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<Args, Ret> Copy for Callback<Args, Ret> {}
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
View File
@@ -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,
};
}