feat: use checkbox logic for chips component

This commit is contained in:
alphafetus committed 2026-01-04 22:56:29 -05:00
1 parent bad5e994b6
commit d9951000e2
4 files changed
+150 -122

No files matched your search

+11 -69
View File
@@ -2,7 +2,8 @@ use crate::common::ValueType;
use crate::components::utils::create_unique_id;
use leptos::prelude::*;
use std::collections::HashMap;
use crate::components::{use_validation_context, FormValidationRegistry, InputState, ValidationState};
use crate::components::ValidationState;
use crate::components::lists::use_selectable_list;
/// # Checkbox Component
/// This component should only be used from within a [`CheckboxList`] component.
@@ -80,17 +81,6 @@ pub fn Checkbox(
}
}
fn value_type_to_vec(value_type: &ValueType) -> Vec<String> {
let ValueType::List(Some(list)) = value_type else {
return Vec::new();
};
list.iter().map(|val| {
val.as_string()
.unwrap_or_default()
.unwrap_or_default()
}).collect::<Vec<String>>()
}
/// # Checkbox List Component
@@ -128,60 +118,12 @@ pub fn CheckboxList(
/// A [`Signal`] indicating whether the list is required. Used for validation.
#[prop(optional, into)] required: Signal<bool>
) -> impl IntoView {
//#region Central List Logic
// Get default values from the data map. This will only run once, when the input is created
// and mounted to the DOM.
let temp_map = data_map.get_untracked();
let origin_value_type = temp_map.get(&data_member)
.unwrap_or(&ValueType::List(None));
let origin_list = value_type_to_vec(origin_value_type);
// "Raw" selected values - just strings, no ValueTyping
let selected_list = RwSignal::new(origin_list);
Effect::new({
let data_member = data_member.clone();
move || {
// Convert selected strings into ValueTypes
let typed_list = selected_list.with(|list|
list.iter().cloned().map(|val|
ValueType::String(Some(val))
).collect::<Vec<ValueType>>()
let controller = use_selectable_list(
data_member.clone(),
data_map.clone(),
required.clone()
);
// Update the map
data_map.update(|map| {
map.insert(data_member.clone(), ValueType::List(Some(typed_list)));
});
}
});
//#endregion
//#region Form Validation
let validation_context = use_validation_context()
.expect("FormValidationRegistry was not found");
let error = Signal::derive(move || {
if required.get() && selected_list.get().len() <= 0 {
ValidationState::Invalid("This field is required.".to_string())
} else {
ValidationState::Valid
}
});
let dirty = RwSignal::new(false);
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
let validator = RwSignal::new(InputState::new(
data_member.clone(),
error.clone(),
dirty.clone()
));
validation_context.validators.update(|list| list.push(validator.clone()));
//#endregion
//#region Render Logic
view! {
<div class="flex flex-col">
@@ -196,13 +138,13 @@ pub fn CheckboxList(
});
let checked = Signal::derive(move || {
selected_list.get().contains(&item.get())
controller.selected_list.get().contains(&item.get())
});
let on_change = move || {
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
selected_list.update(|list| {
controller.selected_list.update(|list| {
let item = item.get();
if list.contains(&item) {
list.retain(|val| *val != item);
@@ -210,7 +152,7 @@ pub fn CheckboxList(
list.push(item);
}
});
dirty.set(true);
controller.dirty.set(true);
};
view! {
@@ -226,10 +168,10 @@ pub fn CheckboxList(
})
.collect_view()}
</div>
<Show when=move || show_errors.get()>
<Show when=move || controller.show_errors.get()>
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
{move || {
match error.get() {
match controller.error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
}
+45 -53
View File
@@ -2,6 +2,8 @@ use crate::common::ValueType;
use crate::components::utils::create_unique_id;
use leptos::prelude::*;
use std::collections::HashMap;
use crate::components::ValidationState;
use crate::components::lists::use_selectable_list;
/// # Chip Component
///
@@ -23,16 +25,16 @@ use std::collections::HashMap;
/// ```
#[component]
pub fn Chip(
#[prop(into)] display_text: String,
#[prop(into)] value: String,
#[prop(into)] display_text: Signal<String>,
#[prop(into)] value: Signal<String>,
#[prop(into)] name: String,
#[prop(into)] on_change: Callback<(), ()>,
#[prop()] checked: Signal<bool>,
#[prop(optional)] disabled: RwSignal<bool>,
#[prop(optional, into)] disabled: Signal<bool>,
) -> impl IntoView {
// Generate a unique id - ensure that the value doesn't contain spaces.
// Without this id, checkbox/radio inputs can interfere with each other.
let id = create_unique_id(&name, &value);
let id = create_unique_id(&name, &value.get_untracked());
view! {
<label for=id>
@@ -72,76 +74,66 @@ pub fn ChipsList(
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
#[prop(into)] displayed_text: Signal<Vec<String>>,
#[prop(into)] values: Signal<Vec<String>>,
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
#[prop(optional, into)] disabled: Signal<bool>,
#[prop(optional, into)] label: String,
#[prop(optional, into)] required: Signal<bool>
) -> impl IntoView {
let controller = use_selectable_list(
data_member.clone(),
data_map.clone(),
required.clone()
);
view! {
<div class="flex flex-col">
<div class="m-1.5 mt-0 mb-0">
<span class="font-bold">{label}</span>
<div class="flex flex-row flex-wrap gap-2 mt-1">
{move || { displayed_text.get().into_iter().zip(values.get())
{displayed_text.get().into_iter().zip(values.get())
.into_iter()
.map(|(text, value)| {
let checked_signal = Signal::derive({
let item_name = value.clone();
let data_member = data_member.clone();
move || {
data_map.get()
.get(&data_member)
.unwrap_or(&ValueType::List(None))
.as_list()
.unwrap_or(Some(vec!()))
.unwrap_or(vec!())
.contains(&ValueType::String(Some(item_name.clone())))
}
let value = RwSignal::new(value);
let checked = Signal::derive(move || {
controller.selected_list.get().contains(&value.get())
});
let on_change = {
let item_name = value.clone();
let data_member = data_member.clone();
move || {
let result = ValueType::String(Some(item_name.clone()));
data_map.update(|map| {
// Attempt to get the value type from the hash map.
match map.get_mut(&data_member) {
Some(value_type) => {
// Attempt to get the value as a list.
match value_type.as_list().unwrap_or(None) {
Some(mut list) => {
// Attempt to find the selected value in the selected list.
// If it's present, remove it, or vice versa.
match list.iter().position(|val| *val == result) {
Some(index) => { list.remove(index); },
None => { list.push(result); }
};
// Update the existing value_type entry in the hash map.
*value_type = ValueType::List(Some(list));
},
// Insert a new value_type entry.
None => { *value_type = ValueType::List(Some(vec![result])); }
};
},
// Insert a new value_type entry.
None => { map.insert(data_member.clone(), ValueType::List(Some(vec![result]))); }
};
});
let on_change = move || {
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
controller.selected_list.update(|list| {
let value = value.get();
if list.contains(&value) {
list.retain(|val| *val != value);
} else {
list.push(value.clone());
}
});
controller.dirty.set(true);
};
view! {
<Chip
checked=checked_signal
name=data_member.clone()
checked=checked
on_change=on_change
value=value.clone()
display_text=text.clone()
value=value
display_text=text
name=data_member.clone()
disabled=disabled
/>
}
})
.collect_view()
.collect_view()}
</div>
<Show when=move || controller.show_errors.get()>
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
{move || {
match controller.error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
}
}}
</div>
</Show>
</div>
}
}
+93
View File
@@ -0,0 +1,93 @@
use leptos::prelude::*;
use std::collections::HashMap;
use crate::common::ValueType;
use crate::components::{use_validation_context, InputState, ValidationState};
pub struct ListController {
pub error: Signal<ValidationState>,
pub dirty: RwSignal<bool>,
pub show_errors: Signal<bool>,
pub validator: RwSignal<InputState>,
pub selected_list: RwSignal<Vec<String>>,
}
fn value_type_to_vec(value_type: &ValueType) -> Vec<String> {
let ValueType::List(Some(list)) = value_type else {
return Vec::new();
};
list.iter().map(|val| {
val.as_string()
.unwrap_or_default()
.unwrap_or_default()
}).collect::<Vec<String>>()
}
pub fn use_selectable_list(
data_member: String,
data_map: RwSignal<HashMap<String, ValueType>>,
required: Signal<bool>
) -> ListController {
//#region Central List Logic
// Get default values from the data map. This will only run once, when the input is created
// and mounted to the DOM.
let temp_map = data_map.get_untracked();
let origin_value_type = temp_map.get(&data_member)
.unwrap_or(&ValueType::List(None));
let origin_list = value_type_to_vec(origin_value_type);
// "Raw" selected values - just strings, no ValueTyping
let selected_list = RwSignal::new(origin_list);
Effect::new({
let data_member = data_member.clone();
move || {
// Convert selected strings into ValueTypes
let typed_list = selected_list.with(|list|
list.iter().cloned().map(|val|
ValueType::String(Some(val))
).collect::<Vec<ValueType>>()
);
// Update the map
data_map.update(|map| {
map.insert(data_member.clone(), ValueType::List(Some(typed_list)));
});
}
});
//#endregion
//#region Form Validation
let validation_context = use_validation_context()
.expect("FormValidationRegistry was not found");
let error = Signal::derive(move || {
if required.get() && selected_list.get().len() <= 0 {
ValidationState::Invalid("This field is required.".to_string())
} else {
ValidationState::Valid
}
});
let dirty = RwSignal::new(false);
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
let validator = RwSignal::new(InputState::new(
data_member.clone(),
error.clone(),
dirty.clone()
));
validation_context.validators.update(|list| list.push(validator.clone()));
//#endregion
ListController {
error,
dirty,
show_errors,
validator,
selected_list,
}
}
+1
View File
@@ -12,6 +12,7 @@ mod select;
mod text_field;
mod utils;
mod validated_form;
mod lists;
pub use self::{
action_button::*, banner::*, checkbox::*, chips::*, dashboard_button::*, loading::*,