use dioxus::prelude::*; use serde::de::DeserializeOwned; use crate::util::error::Error; /// A simple styled form card. Parses form fields into the provided generic type, and calls the /// callback when submitted via button or enter key. #[component] pub fn Form( /// Heading above the form title: String, /// Form content, placed inside a `fieldset` children: Element, /// Text displayed on the form button action_message: String, /// Form-related error. Set to a parsing error on parse failure, or can be used to display any /// submission error. Clears on each submit. Displays as a card between the inputs and action /// button, opens a modal on click. #[props(default)] error: Signal>, /// Whether the form should display in a "loading" state, disables the action button and /// displays a loading animation instead. For best UX, especially on pages the user might open /// directly as opposed to navigate to within the app, initialize to true. This component will /// set it to false when run. This sequence will discourage users from submitting the form /// before the page is ready to handle it. Can also be used to indicate background work is /// taking place in the `callback`, but this is not done automatically. #[props(default)] loading: Signal, /// Content to display below the action button #[props(default)] extra_content: Option, /// Form submission callback. Called with the deserialized form inputs. onsubmit: Callback, ) -> Element where T: DeserializeOwned + 'static, { // Button is initialized to loading, then set back when WASM is finished loading (use_effect // runs only on the client) use_effect(move || loading.set(false)); rsx! { div { class: "card card-xl w-full sm:w-100 sm:h-fit bg-base-200", form { onsubmit: move |evt| { evt.prevent_default(); let data: T = match evt.data.parsed_values() { Ok(data) => data, Err(e) => { let e = Error::message_here(e.to_string()) .with_context("Failed to parse form inputs"); error.set(Some(e)); return; } }; error.set(None); onsubmit.call(data); }, class: "card-body gap-3", h1 { class: "card-title", {title} } fieldset { class: "fieldset pt-0 gap-1", {children} } {error().map(|e| e.as_alert())} div { class: "card-actions gap-4", button { class: "btn btn-primary btn-block", disabled: loading(), if loading() { span { class: "loading loading-dots" } } else { {action_message} } } {extra_content} } } } } }