Make number-type inputs properly optional #60
No files matched your search
@@ -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>>),
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user