feat: implement validation for select component
This commit is contained in:
1 parent
10d001fd6b
commit
b0b6f97b11
5 files changed
+114
-35
No files matched your search
@@ -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
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user