mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
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:
parent
79e18c2d62
commit
d795995e20
11 changed files with 294 additions and 103 deletions
|
@ -89,7 +89,7 @@ fn app() -> Element {
|
||||||
#[component]
|
#[component]
|
||||||
fn CalculatorKey(name: String, onclick: EventHandler<MouseEvent>, children: Element) -> Element {
|
fn CalculatorKey(name: String, onclick: EventHandler<MouseEvent>, children: Element) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
button { class: "calculator-key {name}", onclick: move |e| onclick.call(e), {&children} }
|
button { class: "calculator-key {name}", onclick, {children} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
//!
|
//!
|
||||||
//! ### Events
|
//! ### Events
|
||||||
//! - Handle events with the "onXYZ" syntax
|
//! - 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
|
//! ### Components
|
||||||
|
|
|
@ -27,12 +27,18 @@ fn app() -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[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! {
|
rsx! {
|
||||||
div { "{a}" }
|
div { "{a}" }
|
||||||
div { "{b}" }
|
div { "{b}" }
|
||||||
div { "{c}" }
|
div { "{c}" }
|
||||||
div { {children} }
|
div { {children} }
|
||||||
div { onclick: move |_| onclick.call(()) }
|
div { onclick }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ fn app() -> Element {
|
||||||
div { class: "flex items-start justify-center flex-row",
|
div { class: "flex items-start justify-center flex-row",
|
||||||
SearchBox { country }
|
SearchBox { country }
|
||||||
div { class: "flex flex-wrap w-full px-2",
|
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",
|
div { class: "px-6 py-6 relative",
|
||||||
if let Some(Ok(weather)) = current_weather.read().as_ref() {
|
if let Some(Ok(weather)) = current_weather.read().as_ref() {
|
||||||
CountryData {
|
CountryData {
|
||||||
|
@ -122,7 +122,7 @@ fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
|
||||||
placeholder: "Country name",
|
placeholder: "Country name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
oninput: move |e| input.set(e.value())
|
oninput: move |e: FormEvent| input.set(e.value())
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
class: "w-4 h-4 absolute left-2.5 top-3.5",
|
class: "w-4 h-4 absolute left-2.5 top-3.5",
|
||||||
|
|
|
@ -520,7 +520,7 @@ mod struct_info {
|
||||||
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
|
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,
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct StructInfo<'a> {
|
pub struct StructInfo<'a> {
|
||||||
|
@ -634,12 +634,12 @@ mod struct_info {
|
||||||
|
|
||||||
let event_handlers_fields: Vec<_> = self
|
let event_handlers_fields: Vec<_> = self
|
||||||
.included_fields()
|
.included_fields()
|
||||||
.filter(|f| looks_like_event_handler_type(f.ty))
|
.filter(|f| looks_like_callback_type(f.ty))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let regular_fields: Vec<_> = self
|
let regular_fields: Vec<_> = self
|
||||||
.included_fields()
|
.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| {
|
.map(|f| {
|
||||||
let name = f.name;
|
let name = f.name;
|
||||||
quote!(#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.
|
/// 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 {
|
let Type::Path(ty) = ty else {
|
||||||
return None;
|
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
|
/// Check if a type should be owned by the child component after conversion
|
||||||
fn child_owned_type(ty: &Type) -> bool {
|
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 {
|
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) => {
|
Some(path_without_generics) => {
|
||||||
path_without_generics == parse_quote!(dioxus_core::prelude::ReadOnlySignal)
|
path_without_generics == parse_quote!(dioxus_core::prelude::ReadOnlySignal)
|
||||||
|| path_without_generics == parse_quote!(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());
|
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) => {
|
Some(path_without_generics) => {
|
||||||
path_without_generics == parse_quote!(dioxus_core::prelude::EventHandler)
|
path_without_generics == parse_quote!(dioxus_core::prelude::EventHandler)
|
||||||
|| path_without_generics == parse_quote!(prelude::EventHandler)
|
|| path_without_generics == parse_quote!(prelude::EventHandler)
|
||||||
|| path_without_generics == parse_quote!(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,
|
None => false,
|
||||||
}
|
}
|
||||||
|
@ -1709,20 +1712,21 @@ fn test_looks_like_type() {
|
||||||
ReadOnlySignal<Option<i32>, UnsyncStorage>
|
ReadOnlySignal<Option<i32>, UnsyncStorage>
|
||||||
)));
|
)));
|
||||||
|
|
||||||
assert!(looks_like_event_handler_type(&parse_quote!(
|
assert!(looks_like_callback_type(&parse_quote!(
|
||||||
Option<EventHandler>
|
Option<EventHandler>
|
||||||
)));
|
)));
|
||||||
assert!(looks_like_event_handler_type(&parse_quote!(
|
assert!(looks_like_callback_type(&parse_quote!(
|
||||||
std::option::Option<EventHandler<i32>>
|
std::option::Option<EventHandler<i32>>
|
||||||
)));
|
)));
|
||||||
assert!(looks_like_event_handler_type(&parse_quote!(
|
assert!(looks_like_callback_type(&parse_quote!(
|
||||||
Option<EventHandler<MouseEvent>>
|
Option<EventHandler<MouseEvent>>
|
||||||
)));
|
)));
|
||||||
|
|
||||||
assert!(looks_like_event_handler_type(&parse_quote!(
|
assert!(looks_like_callback_type(&parse_quote!(EventHandler<i32>)));
|
||||||
EventHandler<i32>
|
assert!(looks_like_callback_type(&parse_quote!(EventHandler)));
|
||||||
)));
|
|
||||||
assert!(looks_like_event_handler_type(&parse_quote!(EventHandler)));
|
assert!(looks_like_callback_type(&parse_quote!(Callback<i32>)));
|
||||||
|
assert!(looks_like_callback_type(&parse_quote!(Callback<i32, u32>)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
65
packages/core-macro/tests/event_handler.rs
Normal file
65
packages/core-macro/tests/event_handler.rs
Normal file
|
@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 generational_box::GenerationalBox;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
|
marker::PhantomData,
|
||||||
rc::Rc,
|
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.
|
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust, no_run
|
/// ```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,
|
pub(crate) origin: ScopeId,
|
||||||
/// During diffing components with EventHandler, we move the EventHandler over in place instead of rerunning the child component.
|
/// 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)
|
/// 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>
|
/// 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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("EventHandler")
|
f.debug_struct("Callback")
|
||||||
.field("origin", &self.origin)
|
.field("origin", &self.origin)
|
||||||
.field("callback", &self.callback)
|
.field("callback", &self.callback)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> Default for EventHandler<T> {
|
impl<T: 'static, Ret: Default + 'static> Default for Callback<T, Ret> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
EventHandler::new(|_| {})
|
Callback::new(|_| Ret::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: FnMut(T) + 'static, T: 'static> From<F> for EventHandler<T> {
|
/// A helper trait for [`Callback`]s that allows functions to accept a [`Callback`] that may return an async block which will automatically be spawned.
|
||||||
fn from(f: F) -> Self {
|
///
|
||||||
EventHandler::new(f)
|
/// ```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 {
|
fn clone(&self) -> 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 {
|
fn eq(&self, _: &Self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalListenerCallback<T> = Rc<RefCell<dyn FnMut(T)>>;
|
type ExternalListenerCallback<Args, Ret> = Rc<RefCell<dyn FnMut(Args) -> Ret>>;
|
||||||
|
|
||||||
impl<T: 'static> EventHandler<T> {
|
impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
|
||||||
/// Create a new [`EventHandler`] from an [`FnMut`]. The callback is owned by the current scope and will be dropped when the scope is dropped.
|
/// 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.
|
/// 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]
|
#[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 owner = crate::innerlude::current_owner::<generational_box::UnsyncStorage>();
|
||||||
let callback = owner.insert(Some(Rc::new(RefCell::new(move |event: T| {
|
let callback = owner.insert(Some(
|
||||||
f(event);
|
Rc::new(RefCell::new(move |event: Args| f(event).spawn()))
|
||||||
})) as Rc<RefCell<dyn FnMut(T)>>));
|
as Rc<RefCell<dyn FnMut(Args) -> Ret>>,
|
||||||
EventHandler {
|
));
|
||||||
|
Self {
|
||||||
callback,
|
callback,
|
||||||
origin: current_scope_id().expect("to be in a dioxus runtime"),
|
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]
|
#[track_caller]
|
||||||
pub fn leak(mut f: impl FnMut(T) + 'static) -> EventHandler<T> {
|
pub fn leak(mut f: impl FnMut(Args) -> Ret + 'static) -> Self {
|
||||||
let callback = GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: T| {
|
let callback =
|
||||||
f(event);
|
GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: Args| f(event)))
|
||||||
})) as Rc<RefCell<dyn FnMut(T)>>));
|
as Rc<RefCell<dyn FnMut(Args) -> Ret>>));
|
||||||
EventHandler {
|
Self {
|
||||||
callback,
|
callback,
|
||||||
origin: current_scope_id().expect("to be in a dioxus runtime"),
|
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.
|
/// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic.
|
||||||
pub fn call(&self, event: T) {
|
pub fn call(&self, arguments: Args) -> Ret {
|
||||||
if let Some(callback) = self.callback.read().as_ref() {
|
if let Some(callback) = self.callback.read().as_ref() {
|
||||||
Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin));
|
Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin));
|
||||||
{
|
let value = {
|
||||||
let mut callback = callback.borrow_mut();
|
let mut callback = callback.borrow_mut();
|
||||||
callback(event);
|
callback(arguments)
|
||||||
}
|
};
|
||||||
Runtime::with(|rt| rt.scope_stack.borrow_mut().pop());
|
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
|
/// Forcibly drop the internal handler callback, releasing memory
|
||||||
///
|
///
|
||||||
/// This will force any future calls to "call" to not doing anything
|
/// This will force any future calls to "call" to not doing anything
|
||||||
|
@ -280,22 +395,22 @@ impl<T: 'static> EventHandler<T> {
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// This should only be used by the `rsx!` macro.
|
/// 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));
|
self.callback.set(Some(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// This should only be used by the `rsx!` macro.
|
/// This should only be used by the `rsx!` macro.
|
||||||
pub fn __take(&self) -> ExternalListenerCallback<T> {
|
pub fn __take(&self) -> ExternalListenerCallback<Args, Ret> {
|
||||||
self.callback
|
self.callback
|
||||||
.read()
|
.read()
|
||||||
.clone()
|
.clone()
|
||||||
.expect("EventHandler was manually dropped")
|
.expect("Callback was manually dropped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> std::ops::Deref for EventHandler<T> {
|
impl<Args: 'static, Ret: 'static> std::ops::Deref for Callback<Args, Ret> {
|
||||||
type Target = dyn Fn(T) + 'static;
|
type Target = dyn Fn(Args) -> Ret + 'static;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
// https://github.com/dtolnay/case-studies/tree/master/callable-types
|
// https://github.com/dtolnay/case-studies/tree/master/callable-types
|
||||||
|
|
|
@ -60,10 +60,10 @@ pub(crate) mod innerlude {
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
|
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
|
||||||
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
|
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
|
||||||
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, Mutation, Mutations,
|
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation,
|
||||||
NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, Task, Template,
|
Mutations, NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, SpawnIfAsync,
|
||||||
TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, VText,
|
Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder,
|
||||||
VirtualDom, WriteMutations,
|
VText, VirtualDom, WriteMutations,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The purpose of this module is to alleviate imports of many common types
|
/// 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,
|
provide_context, provide_root_context, queue_effect, remove_future, schedule_update,
|
||||||
schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, try_consume_context,
|
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_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,
|
use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Callback,
|
||||||
ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes,
|
Component, ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment,
|
||||||
IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard,
|
HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties,
|
||||||
ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode,
|
Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template,
|
||||||
Throw, VNode, VNodeInner, VirtualDom,
|
TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,16 +25,35 @@ macro_rules! impl_event {
|
||||||
#[doc(alias = $js_name)]
|
#[doc(alias = $js_name)]
|
||||||
)?
|
)?
|
||||||
#[inline]
|
#[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(
|
::dioxus_core::Attribute::new(
|
||||||
impl_event!(@name $name $($js_name)?),
|
impl_event!(@name $name $($js_name)?),
|
||||||
::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
|
::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
|
||||||
_f(e.map(|e|e.into())).spawn();
|
event_handler.call(e.map(|e| e.into()));
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
).into()
|
).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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ fn Route3(dynamic: String) -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
input {
|
input {
|
||||||
oninput: move |evt| {
|
oninput: move |evt: FormEvent| {
|
||||||
*current_route_str.write() = evt.value();
|
*current_route_str.write() = evt.value();
|
||||||
},
|
},
|
||||||
value: "{current_route_str}"
|
value: "{current_route_str}"
|
||||||
|
|
|
@ -4,7 +4,7 @@ use super::*;
|
||||||
|
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use quote::{quote, quote_spanned};
|
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)]
|
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||||
pub enum AttributeType {
|
pub enum AttributeType {
|
||||||
|
@ -246,8 +246,18 @@ impl ToTokens for ElementAttrNamed {
|
||||||
}
|
}
|
||||||
ElementAttrValue::EventTokens(tokens) => match &self.attr.name {
|
ElementAttrValue::EventTokens(tokens) => match &self.attr.name {
|
||||||
ElementAttrName::BuiltIn(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() =>
|
quote_spanned! { tokens.span() =>
|
||||||
dioxus_elements::events::#name(#tokens)
|
#function(#tokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ElementAttrName::Custom(_) => unreachable!("Handled elsewhere in the macro"),
|
ElementAttrName::Custom(_) => unreachable!("Handled elsewhere in the macro"),
|
||||||
|
|
Loading…
Reference in a new issue