Skip to content

Commit

Permalink
Create closure type; allow async event handlers in props; allow short…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
ealmloff authored Jun 11, 2024
1 parent 79e18c2 commit d795995
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 103 deletions.
2 changes: 1 addition & 1 deletion examples/calculator_mutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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} }
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/rsx_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions examples/shorthand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
4 changes: 2 additions & 2 deletions examples/weather_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down
34 changes: 19 additions & 15 deletions packages/core-macro/src/props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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)
Expand All @@ -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,
}
Expand All @@ -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]
Expand Down
65 changes: 65 additions & 0 deletions packages/core-macro/tests/event_handler.rs
Original file line number Diff line number Diff line change
@@ -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}"
}
}
}
}
Loading

0 comments on commit d795995

Please sign in to comment.