diff --git a/src/components/lists/single/radio.rs b/src/components/lists/single/radio.rs index 86b4ddf..6233bc9 100644 --- a/src/components/lists/single/radio.rs +++ b/src/components/lists/single/radio.rs @@ -2,6 +2,7 @@ use crate::common::ValueType; use crate::components::utils::create_unique_id; use leptos::prelude::*; use std::collections::HashMap; +use leptos::html::Input; use crate::components::{use_validation_context, InputState, ValidationState}; #[component] @@ -20,9 +21,7 @@ pub fn Radio( // Using the "peer" class links this input with the div "peer" at the same level. // By using the "peer-checked" selector this div can toggle states based on this // input's state (checked/unchecked) - class=" - relative peer shrink-0 - hidden" + class="relative peer shrink-0 hidden" type="radio" id=id.clone() name=name @@ -48,6 +47,94 @@ pub fn Radio( } } +/// # Ratio Other Component +/// This is a simple "other" field that contains a text input component. It can be selected using the +/// same logic as the [`Radio`] component, and also has a secondary `RwSignal` that contains the +/// current value of the embedded text input. +/// +/// Example usage: +#[component] +fn RadioOther( + #[prop(into)] name: Signal, + #[prop(into)] checked: Signal, + #[prop(into)] on_change: Callback<()>, + #[prop(into)] disabled: Signal, + #[prop(optional, into)] value: RwSignal, + #[prop(into)] dirty: RwSignal, + #[prop(into)] placeholder: Signal, +) -> impl IntoView { + let id = create_unique_id(&name.get_untracked(), &"other".to_string()); + let input_ref = NodeRef::::new(); + + let input_disabled = Signal::derive(move || { + !checked.get() || disabled.get() + }); + + Effect::new(move || { + checked.get() + .then(|| { + if let Some(node) = input_ref.get() { + node.focus().expect("Couldn't focus input element"); + } + }); + }); + + let on_change_text = move |ev| { + let val = event_target_value(&ev); + + value.set(val); + + on_change.run(()); + }; + + let on_blur_text = move |_| { + dirty.set(true); + }; + + view! { + + } +} + fn validate(value: String, required: bool) -> ValidationState { if required && value.is_empty() { ValidationState::Invalid("This field is required".into()) @@ -64,24 +151,47 @@ pub fn RadioList( #[prop(optional, into)] disabled: Signal, #[prop(optional, into)] label: String, #[prop(optional, into)] required: Signal, + #[prop(optional, into)] other_prompt: Signal, ) -> 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); + let hydrated = RwSignal::new(false); + let selected_value = RwSignal::new(String::new()); + let other_checked = RwSignal::new(false); + let other_value = RwSignal::new(String::new()); - // When the selected value changes, update the data map at the correct location + // Hydration effect that resolves data. Sets up all data properly. + Effect::new({ + let items = items.clone(); + move || { + if hydrated.get() { + return; + } + + let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) { + Some(ValueType::String(Some(value))) => value.clone(), + _ => String::new() + }; + + if !items.contains(&default_value) { + other_checked.set(true); + other_value.set(default_value.clone()); + } + + selected_value.set(default_value); + + hydrated.set(true); + } + }); + + // When the other option is selected and the other value changes, update the selected value. Effect::new(move || { - data_map.update(|map| { - map.insert(data_member.get(), ValueType::String(Some(selected_value.get()))); - }); + other_checked.get() + .then(|| selected_value.set(other_value.get())); }); //#endregion @@ -109,7 +219,15 @@ pub fn RadioList( list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name) }); }); - + + // When the selected value changes, update the data map at the correct location and validate. + Effect::new(move || { + error.set(validate(selected_value.get(), required.get())); + data_map.update(|map| { + map.insert(data_member.get(), ValueType::String(Some(selected_value.get()))); + }); + }); + //#endregion //#region Render Logic @@ -123,12 +241,13 @@ pub fn RadioList( let item = RwSignal::new(item); let checked_signal = Signal::derive(move || { - selected_value.get() == item.get() + selected_value.get() == item.get() && !other_checked.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. + other_checked.set(false); selected_value.set(item.get()); error.set(validate(selected_value.get_untracked(), required.get_untracked())); dirty.set(true); @@ -146,6 +265,18 @@ pub fn RadioList( } }) .collect::>()} + + + +
diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index b60acef..daf1339 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -15,6 +15,7 @@ use crate::common::{ use leptos::prelude::ServerFnError; use leptos::server; +#[cfg(feature = "ssr")] static COMPARISONS_TABLE: &str = "leptos-comparison-test"; /// Helper function to create all sports comparisons. Only used to create initial diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index aaf5d47..d467a2e 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -17,6 +17,7 @@ use crate::common::ExpandableInfo; use leptos::prelude::ServerFnError; use leptos::server; +#[cfg(feature = "ssr")] static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test"; #[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")] diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index b708f36..894d0f2 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -8,7 +8,7 @@ use std::process::{Command, Stdio}; #[cfg(feature = "ssr")] use std::io::Write; -use crate::common::{ComparisonData, ExpandableInfo}; +use crate::common::ExpandableInfo; use crate::pages::UnauthenticatedPage; use crate::pages::utils::get_user_claims; use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm}; @@ -22,7 +22,6 @@ use leptos::task::spawn_local; use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; use std::collections::HashMap; -use crate::pages::api::get_comparisons_categorized; /// # Get Student Info /// Gets a student's information given their `subject`. @@ -210,28 +209,6 @@ pub fn HomePage() -> impl IntoView { }; //#endregion - let comparison_resource = Resource::new( - move || user_subject.get(), - async move |_| get_comparisons_categorized().await - ); - - let comparison_info = RwSignal::new(HashMap::>::new()); - - Effect::new(move || { - match comparison_resource.get() { - Some(Ok(comparisons)) => { - log!("Found comparison info: {:?}", comparisons); - comparison_info.set(comparisons); - } - Some(Err(err)) => { - log!("Error while getting comparisons: {:?}", err); - } - _ => { - log!("An unknown error occurred."); - } - } - }); - view! { @@ -331,6 +308,7 @@ pub fn HomePage() -> impl IntoView { data_member="town" data_map=form_data required=true + other_prompt="Other..." />