feat: implement validation for radio list component
This commit is contained in:
1 parent
b0b6f97b11
commit
bad5e994b6
2 files changed
+101
-54
No files matched your search
+99
-53
@@ -2,16 +2,17 @@ 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::{use_validation_context, InputState, ValidationState};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Radio(
|
pub fn Radio(
|
||||||
#[prop()] checked: Signal<bool>,
|
#[prop()] checked: Signal<bool>,
|
||||||
#[prop(into)] on_change: Callback<(), ()>,
|
#[prop(into)] on_change: Callback<(), ()>,
|
||||||
#[prop(into)] value: String,
|
#[prop(into)] value: Signal<String>,
|
||||||
#[prop(into)] name: String,
|
#[prop(into)] name: Signal<String>,
|
||||||
#[prop(optional, into)] disabled: Signal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let id = create_unique_id(&name, &value);
|
let id = create_unique_id(&name.get_untracked(), &value.get_untracked());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<label for=id class="flex items-center">
|
<label for=id class="flex items-center">
|
||||||
@@ -24,7 +25,7 @@ pub fn Radio(
|
|||||||
hidden"
|
hidden"
|
||||||
type="radio"
|
type="radio"
|
||||||
id=id.clone()
|
id=id.clone()
|
||||||
name=name.clone()
|
name=name
|
||||||
on:change=move |_| on_change.run(())
|
on:change=move |_| on_change.run(())
|
||||||
prop:checked=checked
|
prop:checked=checked
|
||||||
disabled=disabled
|
disabled=disabled
|
||||||
@@ -47,66 +48,111 @@ pub fn Radio(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate(value: String, required: bool) -> ValidationState {
|
||||||
|
if required && value.is_empty() {
|
||||||
|
ValidationState::Invalid("This field is required".into())
|
||||||
|
} else {
|
||||||
|
ValidationState::Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn RadioList(
|
pub fn RadioList(
|
||||||
// #[prop(default = RwSignal::new(String::new()))] selected: RwSignal<ValueType>,
|
#[prop(into)] data_member: Signal<String>,
|
||||||
#[prop(into)] data_member: String,
|
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop()] items: Vec<String>,
|
#[prop()] items: Vec<String>,
|
||||||
#[prop(optional, into)] disabled: Signal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
|
#[prop(optional, into)] required: Signal<bool>,
|
||||||
) -> impl IntoView {
|
) -> 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 default_value = match data_map.get_untracked().get(&data_member.get_untracked()) {
|
||||||
|
Some(ValueType::String(Some(value))) => value.clone(),
|
||||||
|
_ => String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_value = RwSignal::new(default_value);
|
||||||
|
|
||||||
|
// When the selected value changes, update the data map at the correct location
|
||||||
|
Effect::new(move || {
|
||||||
|
data_map.update(|map| {
|
||||||
|
map.insert(data_member.get(), ValueType::String(Some(selected_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 = InputState::new(
|
||||||
|
data_member.get_untracked(),
|
||||||
|
error.clone(),
|
||||||
|
dirty.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
validation_context.validators.update(|list| list.push(RwSignal::new(input_state)));
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region Render Logic
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="m-1.5 mt-0 mb-0">
|
<div class="flex flex-col flex-1">
|
||||||
<span class="font-bold">{label}</span>
|
<div class="m-1.5 mt-0 mb-0">
|
||||||
{items
|
<span class="font-bold">{label}</span>
|
||||||
.into_iter()
|
{items
|
||||||
.map(|item| {
|
.into_iter()
|
||||||
let checked_signal = Signal::derive({
|
.map(|item| {
|
||||||
let item_name = item.clone();
|
let item = RwSignal::new(item);
|
||||||
let data_member = data_member.clone();
|
|
||||||
move || {
|
|
||||||
// Check if the value is contained in the data map's fields
|
|
||||||
let map = data_map.get();
|
|
||||||
let value = map.get(&data_member);
|
|
||||||
let mut result = false;
|
|
||||||
if let Some(val) = value {
|
|
||||||
if val.is_string() {
|
|
||||||
result = val.as_string().unwrap().unwrap() == item_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
let checked_signal = Signal::derive(move || {
|
||||||
|
selected_value.get() == item.get()
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_change = move || {
|
||||||
|
// on_change only runs when the radio button is checked, not when unchecked.
|
||||||
|
// Change the selected value to the new one.
|
||||||
|
selected_value.set(item.get());
|
||||||
|
error.set(validate(selected_value.get_untracked(), required.get_untracked()));
|
||||||
|
dirty.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Radio
|
||||||
|
checked=checked_signal
|
||||||
|
on_change=on_change
|
||||||
|
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||||
|
value=item
|
||||||
|
name=data_member
|
||||||
|
disabled=disabled
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.collect::<Vec<_>>()}
|
||||||
let on_change = {
|
</div>
|
||||||
let item_name = item.clone();
|
<Show when=move || show_errors.get()>
|
||||||
let data_member = data_member.clone();
|
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
||||||
move || {
|
{move || {
|
||||||
// Update the hash map
|
match error.get() {
|
||||||
data_map.update(|map| {
|
ValidationState::Invalid(msg) => msg,
|
||||||
if let Some(val) = map.get_mut(&data_member) {
|
_ => "There is no error - should not see this message.".to_string()
|
||||||
*val = ValueType::String(Some(item_name.clone()));
|
|
||||||
} else {
|
|
||||||
map.insert(data_member.clone(), ValueType::String(Some(item_name.clone())));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
}}
|
||||||
|
</div>
|
||||||
view! {
|
</Show>
|
||||||
<Radio
|
|
||||||
checked=checked_signal
|
|
||||||
on_change=on_change
|
|
||||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
|
||||||
value=item.clone()
|
|
||||||
name=data_member.clone()
|
|
||||||
disabled=disabled
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,8 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
.collect()
|
.collect()
|
||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="town"
|
data_member="town"
|
||||||
data_map = form_data
|
data_map=form_data
|
||||||
|
required=true
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
Reference in new issue
Block a user