feat: implement validation for select component
This commit is contained in:
1 parent
10d001fd6b
commit
b0b6f97b11
5 files changed
+94
-15
No files matched your search
@@ -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 || {
|
||||||
|
|||||||
+83
-11
@@ -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,27 +21,78 @@ 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()
|
||||||
|
.as_string().unwrap_or_default().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
map.insert(data_member.clone(), ValueType::String(Some(target_value)));
|
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-col flex-1">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<label class="flex flex-col flex-1">
|
<label class="flex flex-col flex-1">
|
||||||
<span class="ml-1.5 mb-0 font-bold">{label}</span>
|
<span class="ml-1.5 mb-0 font-bold">{label}</span>
|
||||||
@@ -54,5 +115,16 @@ pub fn Select(
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when=move || show_errors.get()>
|
||||||
|
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
||||||
|
{move || {
|
||||||
|
match error.get() {
|
||||||
|
ValidationState::Invalid(msg) => msg,
|
||||||
|
_ => "There is no error - should not see this message.".to_string()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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`].
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user