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