feat: update checkbox component to use open-ended structures

Commit also includes changes to other components that make unused props optional.
This commit is contained in:
alphafetus committed 2025-09-25 12:31:47 -04:00
1 parent b460c3b3fd
commit 059f904f5d
7 files changed
+79 -39

No files matched your search

+50 -21
View File
@@ -1,4 +1,6 @@
use std::collections::HashMap;
use leptos::prelude::*; use leptos::prelude::*;
use crate::components::ValueType;
#[component] #[component]
pub fn Checkbox( pub fn Checkbox(
@@ -42,7 +44,9 @@ pub fn Checkbox(
#[component] #[component]
pub fn CheckboxList( pub fn CheckboxList(
#[prop(default = RwSignal::new(vec![]))] selected: RwSignal<Vec<String>>, #[prop(optional)] selected: RwSignal<Vec<String>>,
#[prop(into)] data_member: String,
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
#[prop()] items: Vec<String>, #[prop()] items: Vec<String>,
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>, #[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
#[prop(optional, into)] label: String, #[prop(optional, into)] label: String,
@@ -53,32 +57,57 @@ pub fn CheckboxList(
{items {items
.into_iter() .into_iter()
.map(|item| { .map(|item| {
let is_checked = { let checked_signal = Signal::derive({
let item_name = item.clone(); let item_name = item.clone();
move || { selected.get().contains(&item_name) } let data_member = data_member.clone();
move || {
data_map.get() // HashMap
.get(&data_member) // Option<ValueType>
.unwrap_or(&ValueType::List(None)) // ValueType
.as_list() // Result<Option<Vec<ValueType>>, ValueType>
.unwrap_or(Some(vec!())) // Option<Vec<ValueType>>
.unwrap_or(vec!()) // Vec<ValueType>
.contains(&ValueType::String(Some(item_name.clone()))) // bool
}
});
let on_change = {
let item_name = item.clone();
let data_member = data_member.clone();
move || {
let result = ValueType::String(Some(item_name.clone()));
data_map.update(|map| {
if let Some(value_type) = map.get_mut(&data_member) {
// Get the list from the enum
if let Some(mut list) = value_type.as_list().unwrap_or(None) {
// Check if the value exists in the list or not. Both of these branches
// directly update the value within the hash map, otherwise the change
// isn't reactive.
if let Some(index) = list.iter().position(|val| *val == result) {
// Value is already selected, deselect it.
list.remove(index);
} else {
// Value is not selected, so select it.
list.push(result);
}
*value_type = ValueType::List(Some(list));
} else {
// Update the value at the current data_member
let temp = map.get_mut(&data_member.clone()).unwrap();
*temp = ValueType::List(Some(vec![result]));
}
} else {
// Insert a new list into the hash map
map.insert(data_member.clone(), ValueType::List(Some(vec![result])));
}
});
}
}; };
let checked_signal = Signal::derive(is_checked);
// Check if this checkbox should be selected
view! { view! {
<Checkbox <Checkbox
checked=checked_signal checked=checked_signal
on_change={ on_change=on_change
let item_name = item.clone();
move || {
selected
.update(|list| {
if let Some(index) = list
.iter()
.position(|val| *val == item_name)
{
list.remove(index);
} else {
list.push(item_name.clone());
}
});
}
}
// The actual selected values are tracked by this element, not by the checkboxes themselves. // The actual selected values are tracked by this element, not by the checkboxes themselves.
value=item.clone() value=item.clone()
disabled=disabled disabled=disabled
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use leptos::either::either; use leptos::either::either;
use crate::components::{ActionButton, OutlinedTextField}; use crate::components::{ActionButton, OutlinedTextField};
@@ -15,6 +16,7 @@ fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMem
<OutlinedTextField <OutlinedTextField
label=member_info.display_name label=member_info.display_name
data_member=member_info.member_name data_member=member_info.member_name
data_map=RwSignal::new(HashMap::default())
placeholder="testing" placeholder="testing"
value=reactive value=reactive
/> />
+1 -1
View File
@@ -4,7 +4,7 @@ use std::fmt::Display;
/// Defines a set of possible input types. They allow for the type and value of every data member /// Defines a set of possible input types. They allow for the type and value of every data member
/// to be stored within a single `HashMap` or `Vec`. This means that even though all the data is /// to be stored within a single `HashMap` or `Vec`. This means that even though all the data is
/// created as a single type, the values are still able to be type safe. /// created as a single type, the values are still able to be type safe.
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
pub enum ValueType { pub enum ValueType {
/// Indicates a `String` value. /// Indicates a `String` value.
String(Option<String>), String(Option<String>),
+2 -3
View File
@@ -10,14 +10,13 @@ use crate::components::ValueType;
/// by the `value` prop of the component. /// by the `value` prop of the component.
#[component] #[component]
pub fn Select( pub fn Select(
#[prop(default = vec!["Test Value".into()])] value_list: Vec<String>, #[prop()] value_list: Vec<String>,
#[prop(default = RwSignal::new("".into()))] value: RwSignal<String>, #[prop(optional)] value: RwSignal<String>,
#[prop(into)] data_member: String, #[prop(into)] data_member: 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(default = RwSignal::new(false))] disabled: RwSignal<bool>, #[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let on_change = { let on_change = {
let data_member = data_member.clone(); let data_member = data_member.clone();
move |e: Targeted<Event, HtmlSelectElement>| { move |e: Targeted<Event, HtmlSelectElement>| {
+6 -8
View File
@@ -8,7 +8,7 @@ pub fn OutlinedTextField(
#[prop(optional, into)] placeholder: String, #[prop(optional, into)] placeholder: String,
// #[prop()] value: Subfield<TStore, TInner, ValueType>, // #[prop()] value: Subfield<TStore, TInner, ValueType>,
#[prop(into)] data_member: String, // should this be an RwSignal?? #[prop(into)] data_member: String, // should this be an RwSignal??
#[prop(optional)] data_map: RwSignal<HashMap<String, ValueType>>, #[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
#[prop(optional)] value: RwSignal<ValueType>, #[prop(optional)] value: RwSignal<ValueType>,
#[prop(default = "text".to_owned(), into)] input_type: String, #[prop(default = "text".to_owned(), into)] input_type: String,
#[prop(optional)] disabled: RwSignal<bool>, #[prop(optional)] disabled: RwSignal<bool>,
@@ -86,13 +86,11 @@ pub fn OutlinedTextField(
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
prop:name=name prop:name=name
prop:value={ prop:value=move || data_map
data_map .get() // HashMap
.get() // HashMap .get(&data_member) // Option<ValueType>
.get(&data_member) // Option<ValueType> .unwrap_or(&ValueType::String(None)) // ValueType
.unwrap_or(&ValueType::String(None)) // ValueType .to_string()
.to_string()
}
on:input=on_input on:input=on_input
/> />
<div <div
+15 -6
View File
@@ -8,7 +8,7 @@ use serde_dynamo::{from_item, to_item};
use crate::app::Unauthenticated; use crate::app::Unauthenticated;
use crate::common::{ExpandableInfo, StudentInfo, UserClaims}; use crate::common::{ExpandableInfo, StudentInfo, UserClaims};
use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, RadioList, Row, Select, ValueType}; use crate::components::{ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, RadioList, Row, Select, ValueType};
use leptos::leptos_dom::logging::console_log; use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
@@ -92,7 +92,7 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S
pub async fn log_expandable(info: ExpandableInfo) -> Result<(), ServerFnError> { pub async fn log_expandable(info: ExpandableInfo) -> Result<(), ServerFnError> {
console_log(format!("Was given info: {:?}", info).as_str()); console_log(format!("Was given info: {:?}", info).as_str());
// Manually specify the type here since we're not able to infer the type later. // Manually specify the type here since we're not able to infer the type later.
let item: HashMap<String, AttributeValue> = to_item(info.clone())?; let item: HashMap<String, AttributeValue> = to_item(&info)?;
console_log(format!("Converted into a DynamoDB item: {:?}", item).as_str()); console_log(format!("Converted into a DynamoDB item: {:?}", item).as_str());
Ok(()) Ok(())
@@ -181,9 +181,7 @@ pub fn HomePage() -> impl IntoView {
</p> </p>
</Row> </Row>
<Row> <Row>
// Each OutlinedTextField could be simplified later into an enum: // Text { data_member, label, placeholder }
// Text { data_member, label, placeholder }, i.e.
// Text { "first_name", "First Name:", "John" }
<OutlinedTextField <OutlinedTextField
label="First Name:" label="First Name:"
placeholder="John" placeholder="John"
@@ -237,7 +235,6 @@ pub fn HomePage() -> impl IntoView {
/> />
</Row> </Row>
<Row> <Row>
// Each RadioList could be simplified into an enum:
// Radio { data_member, label, items } // Radio { data_member, label, items }
<RadioList <RadioList
label="Town:" label="Town:"
@@ -250,6 +247,18 @@ pub fn HomePage() -> impl IntoView {
data_map = expandable_react.data data_map = expandable_react.data
/> />
</Row> </Row>
<Row>
<CheckboxList
label="Favorite Candies:"
items=vec!["Twizzlers", "Reese's", "Starburst"]
.into_iter()
.map(|s| s.into())
.collect()
disabled=elements_disabled
data_member="favorite_candies"
data_map=expandable_react.data
/>
</Row>
<Row> <Row>
// Select { data_member, label, items } // Select { data_member, label, items }
<Select <Select
+3
View File
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use crate::app::Unauthenticated; use crate::app::Unauthenticated;
use crate::common::UserClaims; use crate::common::UserClaims;
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField, ValueType}; use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField, ValueType};
@@ -24,11 +25,13 @@ pub fn ProviderEntry() -> impl IntoView {
<h1>This is the provider entry page</h1> <h1>This is the provider entry page</h1>
<h2>It would usually have a series of questions that we ask the providers</h2> <h2>It would usually have a series of questions that we ask the providers</h2>
<div> <div>
// Much of this is just a placeholder.
<OutlinedTextField <OutlinedTextField
placeholder = "Testing information..." placeholder = "Testing information..."
name = "testing_input" name = "testing_input"
label = "This is a testing question." label = "This is a testing question."
data_member = "testing" data_member = "testing"
data_map = RwSignal::new(HashMap::new())
value = RwSignal::new(ValueType::String(None)) value = RwSignal::new(ValueType::String(None))
/> />
</div> </div>