feat: implement validation for select component

This commit is contained in:
alphafetus committed 2026-01-02 17:53:28 -05:00
1 parent 10d001fd6b
commit b0b6f97b11
5 files changed
+114 -35

No files matched your search

+2 -2
View File
@@ -2,7 +2,7 @@ use crate::common::ValueType;
use crate::components::utils::create_unique_id; use crate::components::utils::create_unique_id;
use leptos::prelude::*; use leptos::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use crate::components::{FormValidationRegistry, InputState, ValidationState}; use crate::components::{use_validation_context, FormValidationRegistry, InputState, ValidationState};
/// # Checkbox Component /// # Checkbox Component
/// This component should only be used from within a [`CheckboxList`] component. /// This component should only be used from within a [`CheckboxList`] component.
@@ -160,7 +160,7 @@ pub fn CheckboxList(
//#endregion //#endregion
//#region Form Validation //#region Form Validation
let validation_context = use_context::<FormValidationRegistry>() let validation_context = use_validation_context()
.expect("FormValidationRegistry was not found"); .expect("FormValidationRegistry was not found");
let error = Signal::derive(move || { let error = Signal::derive(move || {
+103 -31
View File
@@ -3,6 +3,16 @@ use leptos::ev::{Event, Targeted};
use leptos::prelude::*; use leptos::prelude::*;
use leptos::web_sys::HtmlSelectElement; use leptos::web_sys::HtmlSelectElement;
use std::collections::HashMap; use std::collections::HashMap;
use leptos::logging::{log, warn};
use crate::components::{use_validation_context, InputState, ValidationState};
fn validate(required: bool, value: String, default_value: String) -> ValidationState {
if required && (value.is_empty() || value == default_value) {
ValidationState::Invalid("This input is required.".to_string())
} else {
ValidationState::Valid
}
}
/// A custom-styled select input. /// A custom-styled select input.
/// ///
@@ -11,48 +21,110 @@ use std::collections::HashMap;
#[component] #[component]
pub fn Select( pub fn Select(
#[prop()] value_list: Vec<String>, #[prop()] value_list: Vec<String>,
#[prop(optional, into)] default_value: Signal<String>,
#[prop(optional)] value: RwSignal<String>, #[prop(optional)] value: RwSignal<String>,
#[prop(into)] data_member: String, #[prop(into)] data_member: Signal<String>,
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>, #[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
#[prop(optional, into)] label: String, #[prop(optional, into)] label: String,
#[prop(optional, into)] disabled: Signal<bool>, #[prop(optional, into)] disabled: Signal<bool>,
#[prop(optional, into)] required: Signal<bool>
) -> impl IntoView { ) -> impl IntoView {
let on_change = { //#region Value Logic
let data_member = data_member.clone();
move |e: Targeted<Event, HtmlSelectElement>| { let default_raw_value = if default_value.get_untracked().is_empty() {
let target_value = e.target().value(); data_map.get_untracked()
data_map.update(|map| { .get(&data_member.get_untracked())
if let Some(val) = map.get_mut(&data_member) { .cloned()
*val = ValueType::String(Some(target_value)); .unwrap_or_default()
} else { .as_string().unwrap_or_default().unwrap_or_default()
map.insert(data_member.clone(), ValueType::String(Some(target_value))); } else {
} let default = default_value.get_untracked();
}); if !value_list.contains(&default) {
warn!("The default value {} is not in the list of options. This may cause validation issues.", default);
} }
default
}; };
let raw_value = RwSignal::new(default_raw_value);
Effect::new(move || {
data_map.update(|map| {
map.insert(data_member.get(), ValueType::String(Some(raw_value.get())));
});
});
//#endregion
//#region Validation Logic
let error = RwSignal::new(
validate(required.get_untracked(), raw_value.get_untracked(), default_value.get_untracked())
);
warn!("Current error state: {:?}", error.get_untracked());
let dirty = RwSignal::new(false);
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
let input_state = InputState::new(data_member.get(), error.clone(), dirty.clone());
let validation_context = use_validation_context()
.expect("Could not find FormValidationRegistry context");
validation_context.validators.update(|list| list.push(RwSignal::new(input_state)));
//#endregion
//#region Event Logic
let on_change = move |e: Targeted<Event, HtmlSelectElement>| {
let value = e.target().value();
log!("Found value {}", value);
raw_value.set(value.clone());
error.set(validate(required.get(), raw_value.get(), default_value.get()));
dirty.set(true);
data_map.update(|map| {
if let Some(val) = map.get_mut(&data_member.get()) {
*val = ValueType::String(Some(value.clone()));
} else {
map.insert(data_member.get(), ValueType::String(Some(value)));
}
});
};
//#endregion
view! { view! {
<div class="flex flex-1"> <div class="flex flex-col flex-1">
<label class="flex flex-col flex-1"> <div class="flex flex-1">
<span class="ml-1.5 mb-0 font-bold">{label}</span> <label class="flex flex-col flex-1">
<select <span class="ml-1.5 mb-0 font-bold">{label}</span>
class="relative flex-1 border-2 m-1.5 mt-0 p-1.5 rounded-md <select
transition-border duration-150 class="relative flex-1 border-2 m-1.5 mt-0 p-1.5 rounded-md
border-red-700 bg-transparent transition-border duration-150
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" border-red-700 bg-transparent
on:change:target=on_change disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
disabled = disabled > on:change:target=on_change
// This closure handles the display of the options. disabled = disabled >
// This closure handles the display of the options.
{move || {
value_list.iter().map(
move |value| {
let display = value.to_owned();
view! { <option value={display}>{display.clone()}</option> }
}
).collect::<Vec<_>>()
}}
</select>
</label>
</div>
<Show when=move || show_errors.get()>
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
{move || { {move || {
value_list.iter().map( match error.get() {
move |value| { ValidationState::Invalid(msg) => msg,
let display = value.to_owned(); _ => "There is no error - should not see this message.".to_string()
view! { <option value={display}>{display.clone()}</option> } }
}
).collect::<Vec<_>>()
}} }}
</select> </div>
</label> </Show>
</div> </div>
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@ use leptos::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use leptos::html::Input; use leptos::html::Input;
use leptos::logging::log; use leptos::logging::log;
use crate::components::InputState; use crate::components::{use_validation_context, InputState};
use crate::components::validated_form::{FormValidationRegistry, ValidationState}; use crate::components::validated_form::{FormValidationRegistry, ValidationState};
/// Determines the available types of text fields. /// Determines the available types of text fields.
@@ -68,7 +68,7 @@ pub fn OutlinedTextField(
let input_ref = NodeRef::<Input>::new(); let input_ref = NodeRef::<Input>::new();
// Register this input's validation signal. // Register this input's validation signal.
let validator_context = use_context::<FormValidationRegistry>() let validator_context = use_validation_context()
.expect("FormValidSignal was not found"); .expect("FormValidSignal was not found");
let raw_value = RwSignal::new(data_map.get_untracked() let raw_value = RwSignal::new(data_map.get_untracked()
+5
View File
@@ -56,6 +56,11 @@ pub struct FormValidationRegistry {
pub validators: RwSignal<ValidatorList> pub validators: RwSignal<ValidatorList>
} }
/// Gets the [`FormValidationRegistry`] context if it exists.
pub fn use_validation_context() -> Option<FormValidationRegistry> {
use_context::<FormValidationRegistry>()
}
/// # Validated Form Component /// # Validated Form Component
/// ///
/// A simple HTML `<form>` wrapper that executes validation logic using a [`FormValidationRegistry`]. /// A simple HTML `<form>` wrapper that executes validation logic using a [`FormValidationRegistry`].
+2
View File
@@ -319,9 +319,11 @@ pub fn HomePage() -> impl IntoView {
.into_iter() .into_iter()
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect() .collect()
default_value="Male"
disabled=elements_disabled disabled=elements_disabled
data_member="gender" data_member="gender"
data_map=form_data data_map=form_data
required=true
/> />
</Row> </Row>
<Row> <Row>