use crate::common::ValueType; use crate::components::utils::create_unique_id; use leptos::prelude::*; use std::collections::HashMap; use leptos::html::Input; use crate::components::{use_validation_context, InputState, ValidationState}; #[component] pub fn Radio( #[prop()] checked: Signal, #[prop(into)] on_change: Callback<(), ()>, #[prop(into)] value: Signal, #[prop(into)] name: Signal, #[prop(optional, into)] disabled: Signal, ) -> impl IntoView { let id = create_unique_id(&name.get_untracked(), &value.get_untracked()); view! { } } /// # Ratio Other Component /// This is a simple "other" field that contains a text input component. It can be selected using the /// same logic as the [`Radio`] component, and also has a secondary `RwSignal` that contains the /// current value of the embedded text input. /// /// Example usage: #[component] fn RadioOther( #[prop(into)] name: Signal, #[prop(into)] checked: Signal, #[prop(into)] on_change: Callback<()>, #[prop(into)] disabled: Signal, #[prop(optional, into)] value: RwSignal, #[prop(into)] dirty: RwSignal, #[prop(into)] placeholder: Signal, ) -> impl IntoView { let id = create_unique_id(&name.get_untracked(), &"other".to_string()); let input_ref = NodeRef::::new(); let input_disabled = Signal::derive(move || { !checked.get() || disabled.get() }); Effect::new(move || { checked.get() .then(|| { if let Some(node) = input_ref.get() { node.focus().expect("Couldn't focus input element"); } }); }); let on_change_text = move |ev| { let val = event_target_value(&ev); value.set(val); on_change.run(()); }; let on_blur_text = move |_| { dirty.set(true); }; view! { } } fn validate(value: String, required: bool) -> ValidationState { if required && value.is_empty() { ValidationState::Invalid("This field is required".into()) } else { ValidationState::Valid } } #[component] pub fn RadioList( #[prop(into)] data_member: Signal, #[prop()] data_map: RwSignal>, #[prop()] items: Vec, #[prop(optional, into)] disabled: Signal, #[prop(optional, into)] label: String, #[prop(optional, into)] required: Signal, #[prop(optional, into)] other_prompt: Signal, #[prop(optional, into)] name: Signal ) -> impl IntoView { //#region Value Logic // Store a single signal that is the currently selected value. We can only have one selected // value, so there's no list needed. The default selected value is whatever the map has selected // already, or nothing. let hydrated = RwSignal::new(false); let selected_value = RwSignal::new(String::new()); let other_checked = RwSignal::new(false); let other_value = RwSignal::new(String::new()); // Hydration effect that resolves data. Sets up all data properly. Effect::new({ let items = items.clone(); move || { if hydrated.get() { return; } let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) { Some(ValueType::String(Some(value))) => value.clone(), _ => String::new() }; if !items.contains(&default_value) && !default_value.is_empty() { other_checked.set(true); other_value.set(default_value.clone()); } selected_value.set(default_value); hydrated.set(true); } }); // When the other option is selected and the other value changes, update the selected value. Effect::new(move || { other_checked.get() .then(|| selected_value.set(other_value.get())); }); //#endregion //#region Validation Logic let validation_context = use_validation_context() .expect("Could not find FormValidationRegistry context"); let error = RwSignal::new(validate(selected_value.get_untracked(), required.get_untracked())); let dirty = RwSignal::new(false); let show_errors = Signal::derive(move || { dirty.get() && matches!(error.get(), ValidationState::Invalid(_)) }); let input_state = RwSignal::new(InputState::new( data_member.get_untracked(), error.clone(), dirty.clone() )); validation_context.validators.update(|list| list.push(input_state)); on_cleanup(move || { validation_context.validators.update(|list| { list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name) }); }); // When the selected value changes, update the data map at the correct location and validate. Effect::new(move || { error.set(validate(selected_value.get(), required.get())); data_map.update(|map| { map.insert(data_member.get(), ValueType::String(Some(selected_value.get()))); }); }); //#endregion //#region Render Logic view! {
{label} {items .into_iter() .map(|item| { let item = RwSignal::new(item); let checked_signal = Signal::derive(move || { selected_value.get() == item.get() && !other_checked.get() }); let on_change = move || { other_checked.set(false); selected_value.set(item.get()); error .set( validate( selected_value.get_untracked(), required.get_untracked(), ), ); dirty.set(true); }; // on_change only runs when the radio button is checked, not when unchecked. // Change the selected value to the new one. view! { } }) .collect::>()}
{move || { match error.get() { ValidationState::Invalid(msg) => msg, _ => "There is no error - should not see this message.".to_string(), } }}
} //#endregion }