From 87bd1b50a9bed01706296fdb9465f08ea6d1da1e Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Wed, 8 Jul 2026 21:12:04 -0400 Subject: [PATCH] Add form component --- src/components/form.rs | 101 +++++++++++++++++++++++++++++++++++++++++ src/components/mod.rs | 1 + 2 files changed, 102 insertions(+) create mode 100644 src/components/form.rs diff --git a/src/components/form.rs b/src/components/form.rs new file mode 100644 index 0000000..4579474 --- /dev/null +++ b/src/components/form.rs @@ -0,0 +1,101 @@ +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} + } + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 4641996..318f541 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,2 +1,3 @@ pub mod footer; +pub mod form; pub mod form_input;