DioxusLabs / dioxus

Fullstack app framework for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
20.72k stars 799 forks source link

Support prop extensions for callbacks #2467

Open Zagitta opened 4 months ago

Zagitta commented 4 months ago

Feature Request

It would be nice to be able to create wrapper components that also can pass down callbacks. For example, I'm creating a small select wrapper with some tailwind styling and a label:

#[derive(Props, PartialEq, Clone)]
pub struct SelectProps {
    label: String,
    variants: Vec<(String, String)>,
    // You can extend a specific element or global attributes
    #[props(extends = GlobalAttributes, extends = select)]
    attributes: Vec<Attribute>,
}

pub fn Select(props: SelectProps) -> Element {
    let id_att = props
        .attributes
        .iter()
        .find_map(|att| (att.name == "id").then_some(att.value.clone()));
    rsx!(
        div {
            label { r#for: id_att, class: "text-gray-700 dark:text-gray-200", "{props.label}" }
            select {
                class: "block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring",
                ..props.attributes,
                for (value , name) in props.variants {
                    option { value, "{name}" }
                }
            }
        }
    )
}

Which I then would like to use like this:

#[component]
fn Example() -> Element {
    rsx!(
        form {
            Select {
                label: "Example",
                variants: vec![
                    ("Hello".to_string(), "Hello".to_string()),
                    ("Hello".to_string(), "Hello".to_string()),
                ],
                onchange: move |evt| info!("{:?}", evt)
            }
        }
    )
}

However that that gives the following unexpected error:

no method named `onchange` found for struct `SelectPropsBuilder` in the current scope
method not found in `SelectPropsBuilder<((String,), (Vec<(String, String)>,))>

Ideally, this should just work and pass down the callback as expected without requiring anything from the user.

dxps commented 1 week ago

This is a really needed feature. Is it considered as part of the upcoming ver. 0.6, please?

Andrew15-5 commented 1 week ago

Here is a fixed example from here:

#[derive(Props, PartialEq, Clone)]
struct ButtonProps {
    #[props(extends = GlobalAttributes)]
    attributes: Vec<Attribute>,
    onclick: Option<EventHandler<MouseEvent>>,
}

#[component]
fn ResetButton(props: ButtonProps) -> Element {
    rsx! {
        button {
            class: "reset",
            onclick: move |event| if let Some(f) = props.onclick.as_ref() { f(event) }
            ..props.attributes,
            img { src: RESET_ICON }
        }
    }
}

You would have to do this manually for every callback/"on*" attribute. I doubt this will be included in the 0.6.

dxps commented 5 days ago

@Andrew15-5 Thanks, Andrew! That's great for event handlers.

And maybe I'm missing something, but this looks pretty similar with the example - that FancyButton - from official Event Handlers page, except that in this case an Option of EventHandler<MouseEvent> is used.

And revisiting that page, I realised that there is a simple way to pass a closure that needs to call an async fn. Here is a recent example:

#[derive(Props, PartialEq, Clone)]
pub struct ConfirmDeleteModalProps {
    pub title: String,
    pub content: String,
    pub action: Signal<Action>,
    pub show_delete_confirm: Signal<bool>,
    pub delete_handler: EventHandler,
}

#[component]
pub fn ConfirmDeleteModal(props: ConfirmDeleteModalProps) -> Element {
    let ConfirmDeleteModalProps {
        title,
        content,
        mut action,
        mut show_delete_confirm,
        delete_handler,
    } = props;
    rsx! {
        div { class: "fixed inset-0 p-4 flex flex-wrap justify-center items-center w-full h-full z-[1000] before:fixed before:inset-0 before:w-full before:h-full before:bg-[rgba(0,0,0,0.5)] overflow-auto font-[sans-serif]",
            div { class: "w-full max-w-lg bg-white shadow-lg rounded-lg p-8 relative",
                div {
                    h4 { class: "text-sm text-gray-800 font-semibold", {title} }
                    p { class: "text-sm text-gray-600 mt-4", { content } }
                }
                div { class: "flex justify-between mt-8",
                    button {
                        class: "text-red-600 bg-red-50 hover:text-red-800 hover:bg-red-100 drop-shadow-sm px-4 rounded-md",
                        onclick: move |_| {
                            show_delete_confirm.set(false);
                            action.set(Action::Delete);
                            delete_handler(());
                        },
                        "Delete"
                    }
                    button {
                        class: "bg-gray-100 bg-green-100 enabled:hover:bg-green-100 disabled:text-gray-400 hover:disabled:bg-gray-100 drop-shadow-sm px-4 rounded-md",
                        onclick: move |_| {
                            show_delete_confirm.set(false);
                        },
                        "Cancel"
                    }
                }
            }
        }
    }
}

And that reusable component being used as such:

#[derive(PartialEq, Props, Clone)]
pub struct AttributeDefEditPageProps {
    attr_def_id: Id,
}

#[component]
pub fn AttributeDefPage(props: AttributeDefEditPageProps) -> Element {
    //
    let mut action = use_signal(|| Action::View);
    let mut show_delete_confirm = use_signal(|| false);
    rsx! {
        div { class: "flex flex-col min-h-screen bg-gray-100",
            ...
            if show_delete_confirm() {
                ConfirmDeleteModal {
                    title: "Confirm Delete",
                    content: "Are you sure you want to delete this attribute definition?",
                    action,
                    show_delete_confirm,
                    delete_handler: move |_| {
                        spawn(async move {
                            log::debug!("Calling handle_delete ...");
                            handle_delete(&id(), action_done, err).await;
                        });
                    }
                }
            }
        }
    }
}

async fn handle_delete(id: &Id, mut saved: Signal<bool>, mut err: Signal<Option<String>>) {
    //
    log::debug!(">>> Deleting attribute definition: {:?}", id);
    match remove_attr_def(id.clone()).await {
        Ok(_) => {
            saved.set(true);
            err.set(None);
        }
        Err(e) => {
            saved.set(false);
            err.set(Some(e.to_string()));
        }
    }
}