dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Spreading doesn't work with events

Open chrivers opened this issue 8 months ago • 2 comments

Feature Request

This is a spin-off from #4011 ("Spread doesn't work with events").

When trying to create reusable components, it's quite jarring that event handlers are somehow not considered attributes.

I realize there's probably some Arcane Arts going on behind the scenes to make event handlers work, but it still leaves library authors in a complicated position.

Here's a minimal example:

use dioxus::prelude::*;

const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");

fn main() {
    dioxus::launch(App);
}

#[component]
fn App() -> Element {
    rsx! {
        document::Link { rel: "icon", href: FAVICON }
        document::Link { rel: "stylesheet", href: MAIN_CSS }
        Hero {}
    }
}

#[derive(Clone, Props, PartialEq)]
struct CustomButtonProps {
    #[props(extends = button, extends = GlobalAttributes)]
    pub attributes: Vec<Attribute>,
}

#[component]
pub fn CustomButton(props: CustomButtonProps) -> Element {
    rsx! {
        button {
            ..props.attributes
        }
    }
}

#[component]
pub fn Hero() -> Element {
    rsx! {
        CustomButton {
            /* vvvv -- compile fails here -- vvvv */
            onclick: move |_| { /* useful work.. */ }, 
        }
    }
}

We get this error message:

16:15:59 [cargo] error[E0599]: no method named `onclick` found for struct `CustomButtonPropsBuilder` in the current scope
  --> bug/src/main.rs:39:13
   |
19 |   #[derive(Clone, Props, PartialEq)]
   |                   ----- method `onclick` not found for this struct
...
37 | /     rsx! {
38 | |         CustomButton {
39 | |             onclick: move |_| { /* yep */ },
   | |            -^^^^^^^ method not found in `CustomButtonPropsBuilder<((),)>`
   | |____________|
   |

From what I can tell about the internals (I'm still trying to understand how it all works), macros are used to derive GlobalAttributes (actually, GlobalAttributesExtension, if I'm not mistaken?), and the same for button-specific attributes).

And events are not covered by this mechanism. Is that mental model accurate?

In any case, it's a big blocker for building any kind of reusable higher-level components. Currently, each and every single potentially-useful event will have to be manually implemented as an Option<EventHandler<T>> (with a concrete T type), and then if-let-Some-style handled in the body.

Needless to say, this doesn't really scale 😄

chrivers avatar Apr 19 '25 14:04 chrivers

@ealmloff You've done excellent work in improving prop spreading.

Do you happen to know what the challenge is here? You mentioned in #4011 that this might not be too hard to fix. Or was that referring to something else?

Are events handled very differently from other attributes?

chrivers avatar Apr 22 '25 13:04 chrivers

Are events handled very differently from other attributes?

The type inferences on event handlers is slightly worse for components vs elements:

use dioxus::prelude::*;

fn main() {
    dioxus::launch(|| {
        rsx! {
          App {
            // This does not compile
            onclick: |evt| println!("{:?}", evt.data()),
          }
          button {
            // This does compile
            onclick: |evt| println!("{:?}", evt.data()),
          }
        }
    });
}

#[component]
fn App(onclick: EventHandler<MouseEvent>) -> Element {
    rsx! {
      button {
        onclick
      }
    }
}

In terms of spreading, they should be implemented in a very similar way. We just need to centralize the list of events and add them to the GlobalAttributes trait. Since EventHandler is copy and owned by the props themselves, we will need to generate an owner in the props that derives spread to hold the event handler and handle memorization in place like we do for manual event handlers in components:

impl dioxus_core::prelude::Properties for AppProps
where
    Self: Clone,
{
    type Builder = AppPropsBuilder<((),)>;
    fn builder() -> Self::Builder {
        AppProps::builder()
    }
    fn memoize(&mut self, new: &Self) -> bool {
        let equal = self == new;
        self.onclick.__point_to(&new.onclick);
        if !equal {
            let new_clone = new.clone();
        }
        equal
    }
}

ealmloff avatar Apr 23 '25 13:04 ealmloff