use leptos::logging::log; use leptos::prelude::*; use leptos::web_sys::SubmitEvent; use crate::components::ActionButton; /// Indicates the current state of an input. #[derive(Default, Debug, Clone)] pub enum ValidationState { /// A valid input. #[default] Valid, /// An invalid input with an error message. Invalid(String) } /// A struct that contains all information required for a form to make real-time validation /// decisions, including display of error messages. #[derive(Debug, Clone)] pub struct InputState { /// The name of the input. This is used to identify the input in the form. pub input_name: String, /// Indicates the current state of the input. error: Signal, /// Indicates whether the form has requested the error state. This means that the input /// should show their error appearances immediately. form_requested_validation: RwSignal, } impl InputState { /// Creates a new [`InputState`] with the given error and form requested validation signals. pub fn new( name: impl Into, error: impl Into>, form_requested_errors: RwSignal ) -> Self { Self { input_name: name.into(), error: error.into(), form_requested_validation: form_requested_errors } } pub fn error(&self) -> Signal { self.error.clone() } } /// A list of [`InputState`]s. pub type ValidatorList = Vec>; /// A struct that keeps track of all components that must be validated. /// All of our custom-made components must be placed within an area that contains context with /// this struct. #[derive(Debug, Clone)] pub struct FormValidationRegistry { pub validators: RwSignal } /// # Validated Form Component /// /// A simple HTML `
` wrapper that executes validation logic using a [`FormValidationRegistry`]. /// It will ensure that any invalid inputs are caught before submission. All input components that /// are used within this form should call `use_context::()` to register /// themselves with the form. /// /// Example usage in the view macro: /// ``` /// view! { /// /// {/* As many input components as you want */} /// /// } /// ``` /// /// Example input component implementation: /// ``` /// fn SomeComponent() -> impl IntoView { /// // Get FormValidationContext /// let context = use_context::() /// .expect("Couldn't find FormValidationRegsitry context"); /// /// // Register with the form /// let error = RwSignal::new(ValidationState::Valid); /// let form_requested_errors = RwSignal::new(false); /// let state = InputState::new( /// "the_input_name", /// error.clone(), /// form_requested_errors.clone() /// ); /// context.validators.update(|list| list.push(RwSignal::new(state))); /// /// /* Other input view logic */ /// } /// ``` #[component] pub fn ValidatedForm( children: Children, /// A callback that runs when the form successfully validates. #[prop(into)] on_submit: Callback<()>, ) -> impl IntoView { let validators = RwSignal::new(vec![]); // Provide context to all children nested within this component. They will be able to access // this no matter how nested they are. provide_context(FormValidationRegistry {validators}); let can_submit = Memo::new(move |_| { log!("Checking {} inputs...", validators.get().len()); validators .with(|list| { list.iter() // TODO all() short circuits on the first false .all(|v| { // Not sure how this will work reactively. let result = matches!(v.get().error().get(), ValidationState::Valid); v.update(|validator| validator.form_requested_validation.set(true)); result }) }) }); let submit_success_event = move |e: SubmitEvent| { e.prevent_default(); if !can_submit.get() { log!("Form failed to submit, there are errors in form inputs."); return; } log!("Form validation successful."); on_submit.run(()); }; view! { {children()} "Submit form (but actually)" } }