diff --git a/src/components/checkbox.rs b/src/components/checkbox.rs index 287b1a7..80254d3 100644 --- a/src/components/checkbox.rs +++ b/src/components/checkbox.rs @@ -2,16 +2,17 @@ use crate::common::ValueType; use crate::components::utils::create_unique_id; use leptos::prelude::*; use std::collections::HashMap; +use crate::components::{FormValidationRegistry, InputState, ValidationState}; #[component] pub fn Checkbox( #[prop()] checked: Signal, #[prop(into)] on_change: Callback<(), ()>, - #[prop(into)] value: String, + #[prop(into)] value: Signal, #[prop(into)] name: String, #[prop(into)] disabled: Signal, ) -> impl IntoView { - let id = create_unique_id(&name, &value); + let id = create_unique_id(&name, &value.get_untracked()); view! { } } +fn value_type_to_vec(value_type: &ValueType) -> Vec { + 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::>() +} + #[component] pub fn CheckboxList( #[prop(optional)] selected: RwSignal>, @@ -55,71 +68,117 @@ pub fn CheckboxList( #[prop()] items: Vec, #[prop(optional, into)] disabled: Signal, #[prop(optional, into)] label: String, + #[prop(optional, into)] required: Signal ) -> 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::>() + ); + + // 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_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! { -
- {label} - {items - .into_iter() - .map(|item| { - let checked_signal = Signal::derive({ - let item_name = item.clone(); - let data_member = data_member.clone(); - move || { - data_map.get() // HashMap - .get(&data_member) // Option - .unwrap_or(&ValueType::List(None)) // ValueType - .as_list() // Result>, ValueType> - .unwrap_or(Some(vec!())) // Option> - .unwrap_or(vec!()) // Vec - .contains(&ValueType::String(Some(item_name.clone()))) // bool - } - }); +
+
+ {label} + {items + .into_iter() + .map(|item| { + let item = Signal::derive({ + let cloned = item.clone(); + move || cloned.clone() + }); - 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| { - // 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 checked = Signal::derive(move || { + 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| { + let item = item.get(); + if list.contains(&item) { + list.retain(|val| *val != item); + } else { + list.push(item); + } }); - } - }; + dirty.set(true); + }; - view! { - - } - }) - .collect::>()} + view! { + + } + }) + .collect_view()} +
+ +
+ {move || { + match error.get() { + ValidationState::Invalid(msg) => msg, + _ => "There is no error - should not see this message.".to_string() + } + }} +
+
} + //#endregion } diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 11be7bf..0fe5410 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,7 +1,6 @@ // Server dependencies #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue}; - #[cfg(feature = "ssr")] use serde_dynamo::{from_item, to_item}; #[cfg(feature = "ssr")] @@ -310,6 +309,7 @@ pub fn HomePage() -> impl IntoView { disabled=elements_disabled data_member="favorite_candies" data_map=form_data + required=true /> diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index 6fc24a2..27f1bc6 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use leptos::either::Either; use leptos::html::Dialog; use leptos::logging::log; @@ -5,9 +6,8 @@ use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; -use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType}; +use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, ValidatedForm}; use super::UnauthenticatedPage; -use traits::{AsReactive, ReactiveCapture}; use crate::common::ComparisonData; use crate::pages::utils::get_user_claims; use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, CreateTestComparisons, RegisterScholarship, DeleteProviderScholarship}; @@ -357,14 +357,14 @@ fn ScholarshipForm( _ => false } }); - let result_msg = Signal::derive(move || { - match submit_status.get() { - SubmitStatus::Idle => "".into(), - SubmitStatus::Sending => "Sending submission...".into(), - SubmitStatus::Success => "Success".into(), - SubmitStatus::Error(msg) => format!("Error: {}", msg), - } - }); + // let result_msg = Signal::derive(move || { + // match submit_status.get() { + // SubmitStatus::Idle => "".into(), + // SubmitStatus::Sending => "Sending submission...".into(), + // SubmitStatus::Success => "Success".into(), + // SubmitStatus::Error(msg) => format!("Error: {}", msg), + // } + // }); let submit_action = ServerAction::::new(); @@ -406,6 +406,14 @@ fn ScholarshipForm( } } ); + + let form_data = RwSignal::new(HashMap::new()); + + Effect::new(move || { + if let Some(Ok(scholarship)) = scholarship_info.get() { + form_data.set(scholarship.data); + } + }); let comparison_ids = Memo::new(move |_| { comparison_list.get().map(|list| list.iter() @@ -424,7 +432,17 @@ fn ScholarshipForm( // We'll collect the scholarship's name, num_awards, amount_per_award, total_awards, // fafsa_required, award_to, transcript_required, recipient_selection, essay_requirement (and prompt) // award_night_remarks - + + let on_submit = move |_| { + let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default()); + info.data = form_data.get(); + + log!("Map values: {:?}", info.data); + submit_action.dispatch(CreateScholarshipInfo { + info + }); + }; + view! { "Choose a scholarship from the right, or create a new one."

} > - {move || { - scholarship_info.get() - .map(|res_scholarship| { - match res_scholarship { - Ok(scholarship) => { - let reactive_info = scholarship.as_reactive(); - - Either::Left(view! { - -

- "Scholarship Info Form (test)" -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "Submit" - - -

{result_msg}

-
- }) + + {move || { + scholarship_info.get() + .map(|res_scholarship| { + match res_scholarship { + Ok(scholarship) => { + Either::Left(view! { + +

+ "Scholarship Info Form (test)" +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + }) + } + Err(err) => { + Either::Right(view! { +

"Error while getting scholarship: "{err.to_string()}

+ }) + } } - Err(err) => { - Either::Right(view! { -

"Error while getting scholarship: "{err.to_string()}

- }) - } - } - }).collect_view() - }} + }).collect_view() + }} +