fix: allow users to type decimal values

This commit is contained in:
alphafetus committed 2026-02-11 11:45:47 -05:00
1 parent f0db43817c
commit e1de6064d9
3 files changed
+90 -80

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ use std::fmt::Display;
pub enum ValueType { pub enum ValueType {
/// Indicates a `String` value. /// Indicates a `String` value.
String(Option<String>), String(Option<String>),
/// Indicates an `i32` value. /// Indicates an `i32` value. /// Indicates an `f32` value.
Number(Option<String>), Number(Option<String>),
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`. /// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
List(Option<Vec<ValueType>>), List(Option<Vec<ValueType>>),
+37 -31
View File
@@ -2,6 +2,7 @@ use crate::common::ValueType;
use leptos::prelude::*; use leptos::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use leptos::html::Input; use leptos::html::Input;
use leptos::logging::debug_log;
use crate::components::{use_validation_context, InputState}; use crate::components::{use_validation_context, InputState};
use crate::components::validated_form::ValidationState; use crate::components::validated_form::ValidationState;
@@ -66,7 +67,7 @@ fn validate_number(input: &str) -> ValidationState {
#[component] #[component]
pub fn OutlinedTextField( pub fn OutlinedTextField(
#[prop(optional, into)] placeholder: String, #[prop(optional, into)] placeholder: String,
#[prop(into)] data_member: String, // should this be an RwSignal?? #[prop(into)] data_member: String,
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>, #[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
#[prop(optional, into)] input_type: Signal<TextFieldType>, #[prop(optional, into)] input_type: Signal<TextFieldType>,
#[prop(optional, into)] disabled: Signal<bool>, #[prop(optional, into)] disabled: Signal<bool>,
@@ -75,30 +76,28 @@ pub fn OutlinedTextField(
#[prop(optional, into)] required: Signal<bool> #[prop(optional, into)] required: Signal<bool>
) -> impl IntoView { ) -> impl IntoView {
let input_ref = NodeRef::<Input>::new(); let input_ref = NodeRef::<Input>::new();
let data_member = StoredValue::new(data_member);
// Register this input's validation signal. // Register this input's validation signal.
let validator_context = use_validation_context() let validator_context = use_validation_context()
.expect("FormValidSignal was not found"); .expect("FormValidSignal was not found");
let raw_value = Signal::derive({ let display_value = RwSignal::new(
let data_member = data_member.clone(); data_map.get_untracked()
move || .get(&data_member.get_value())
data_map .map(|v| v.to_string())
.get() .unwrap_or_default()
.get(&data_member) );
.unwrap_or(&ValueType::String(None))
.to_string()
});
let dirty = RwSignal::new(false); let dirty = RwSignal::new(false);
let error = Signal::derive(move || let error = Signal::derive(move ||
validate(required.get(), &raw_value.get(), &input_type.get()) validate(required.get(), &display_value.get(), &input_type.get())
); );
let show_errors = Signal::derive(move || { let show_errors = Signal::derive(move || {
dirty.get() && matches!(error.get(), ValidationState::Invalid(_)) dirty.get() && matches!(error.get(), ValidationState::Invalid(_))
}); });
let validator = RwSignal::new(InputState::new(data_member.clone(), error.clone(), dirty.clone())); let validator = RwSignal::new(InputState::new(data_member.get_value(), error.clone(), dirty.clone()));
validator_context.validators.update(|list| list.push(validator)); validator_context.validators.update(|list| list.push(validator));
@@ -108,35 +107,42 @@ pub fn OutlinedTextField(
}); });
}); });
let str_type = Signal::derive(move || match input_type.get_untracked() {
TextFieldType::Number => "number",
_ => "text" // This is fine even for emails, as we're doing our own validation.
});
let on_blur = move |_| { let on_blur = move |_| {
// Check validity of input's current value. The value will be updated by on_input, // Check validity of input's current value. The value will be updated by on_input,
// and it should be a raw input (not parsed into a ValueType). // and it should be a raw input (not parsed into a ValueType).
dirty.set(true); dirty.set(true);
}; };
// Syncs data from the map into the display_value, only when the map is actually changed.
Effect::new(move || {
let new_value = data_map.get()
.get(&data_member.get_value())
.map(|v| v.to_string())
.unwrap_or_default();
if new_value != display_value.get_untracked() {
debug_log!("Found value from map: {}", new_value);
display_value.set(new_value);
}
});
// This function parses the input into the correct type. It only accepts numbers or strings as // This function parses the input into the correct type. It only accepts numbers or strings as
// valid types, and parses them accordingly. // valid types, and parses them accordingly.
let on_input = { let on_input = move |e| {
let data_member = data_member.clone(); let to_parse = event_target_value(&e);
move |e| { debug_log!("Setting value to display_value: {}", to_parse);
let to_parse = event_target_value(&e); display_value.set(to_parse.clone());
// Push the new value to the data map. This happens regardless of whether the value parses
// or not.
data_map.update(|map| {
let into_map = match input_type.get() { let into_map = match input_type.get() {
TextFieldType::Number => ValueType::Number(Some(to_parse)), TextFieldType::Number => ValueType::Number(Some(to_parse)),
_ => ValueType::String(Some(to_parse)) _ => ValueType::String(Some(to_parse))
}; };
// Edit the data map. Note that this should be uncoupled in the future - we don't want map.insert(data_member.get_value(), into_map);
// to update the map inside the component definition, but inside the page/form that });
// created this input.
data_map.update(|map| {
map.insert(data_member.clone(), into_map);
});
}
}; };
view! { view! {
@@ -149,11 +155,11 @@ pub fn OutlinedTextField(
transition-all duration-150 transition-all duration-150
border-red-700 bg-transparent border-red-700 bg-transparent
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
r#type=str_type r#type="text"
disabled=disabled disabled=disabled
placeholder=placeholder placeholder=placeholder
prop:name=name prop:name=name
prop:value=raw_value prop:value=display_value
on:input=on_input on:input=on_input
on:blur=on_blur on:blur=on_blur
/> />
+52 -48
View File
@@ -7,53 +7,57 @@ pub fn StudentAcademicsPage() -> impl IntoView {
let controller = use_student_form("academics"); let controller = use_student_form("academics");
view! { view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading> <Suspense fallback=Loading>
<div class="flex flex-1" /> {move || controller.data_resource.get().map(|_|
<div class="flex flex-col flex-2 mt-6"> view! {
<ValidatedForm title="Academic Information" on_submit=controller.submit_action> <div class="flex flex-1" />
<OutlinedTextField <div class="flex flex-col flex-2 mt-6">
label="Unweighted GPA:" <ValidatedForm title="Academic Information" on_submit=controller.submit_action>
data_map=controller.data_map <OutlinedTextField
data_member="unweighted_gpa" label="Unweighted GPA:"
placeholder="4.0" data_map=controller.data_map
required=true data_member="unweighted_gpa"
input_type=TextFieldType::Number placeholder="4.0"
/> required=true
<OutlinedTextField input_type=TextFieldType::Number
label="Weighted GPA:" />
data_map=controller.data_map <OutlinedTextField
data_member="weighted_gpa" label="Weighted GPA:"
placeholder="4.5" data_map=controller.data_map
required=true data_member="weighted_gpa"
input_type=TextFieldType::Number placeholder="4.5"
/> required=true
<OutlinedTextField input_type=TextFieldType::Number
label="Highest SAT Score:" />
data_map=controller.data_map <OutlinedTextField
data_member="sat_score" label="Highest SAT Score:"
placeholder="1600" data_map=controller.data_map
required=false data_member="sat_score"
input_type=TextFieldType::Number placeholder="1600"
/> required=false
<OutlinedTextField input_type=TextFieldType::Number
label="Highest ACT Score:" />
data_map=controller.data_map <OutlinedTextField
data_member="act_score" label="Highest ACT Score:"
placeholder="36" data_map=controller.data_map
required=false data_member="act_score"
input_type=TextFieldType::Number placeholder="36"
/> required=false
<OutlinedTextField input_type=TextFieldType::Number
label="Academic Honors:" />
data_map=controller.data_map <OutlinedTextField
data_member="academic_honors" label="Academic Honors:"
placeholder="TBD" data_map=controller.data_map
required=false data_member="academic_honors"
input_type=TextFieldType::Text placeholder="TBD"
/> required=false
</ValidatedForm> input_type=TextFieldType::Text
</div> />
<div class="flex flex-1" /> </ValidatedForm>
</Show> </div>
<div class="flex flex-1" />
}).collect_view()
}
</Suspense>
} }
} }