Merge pull request #60 from R15-Software-Dev/fix/optional-number-inputs
Make number-type inputs properly optional
This commit was merged in pull request #60.
This commit is contained in:
9 files changed
+125
-98
No files matched your search
+2
-8
@@ -136,14 +136,8 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
path=path!("additional/work-experience")
|
||||
view=student::StudentWorkExperiencePage
|
||||
/>
|
||||
<Route
|
||||
path=path!("additional/university")
|
||||
view=student::StudentUniversityPage
|
||||
/>
|
||||
<Route
|
||||
path=path!("additional/family-info")
|
||||
view=student::StudentFamilyPage
|
||||
/>
|
||||
<Route path=path!("additional/university") view=student::StudentUniversityPage />
|
||||
<Route path=path!("additional/family-info") view=student::StudentFamilyPage />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
|
||||
+20
-10
@@ -36,7 +36,7 @@ impl InputType {
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
{match self {
|
||||
InputType::Text(member, label, placeholder, required) =>
|
||||
InputType::Text(member, label, placeholder, required) => {
|
||||
view! {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
@@ -47,8 +47,10 @@ impl InputType {
|
||||
required=required
|
||||
/>
|
||||
</Row>
|
||||
}.into_any(),
|
||||
InputType::Number(member, label, placeholder, required) =>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
InputType::Number(member, label, placeholder, required) => {
|
||||
view! {
|
||||
<OutlinedTextField
|
||||
label=label.clone()
|
||||
@@ -58,8 +60,10 @@ impl InputType {
|
||||
input_type=TextFieldType::Number
|
||||
required=required
|
||||
/>
|
||||
}.into_any(),
|
||||
InputType::Checkbox(member, label, options, required) =>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
InputType::Checkbox(member, label, options, required) => {
|
||||
view! {
|
||||
<CheckboxList
|
||||
label=label.clone()
|
||||
@@ -69,8 +73,10 @@ impl InputType {
|
||||
data_map=data_map
|
||||
required=required
|
||||
/>
|
||||
}.into_any(),
|
||||
InputType::Radio(member, label, options, required) =>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
InputType::Radio(member, label, options, required) => {
|
||||
view! {
|
||||
<RadioList
|
||||
label=label.clone()
|
||||
@@ -79,8 +85,10 @@ impl InputType {
|
||||
items=options.clone()
|
||||
required=required
|
||||
/>
|
||||
}.into_any(),
|
||||
InputType::Select(member, label, options, required) =>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
InputType::Select(member, label, options, required) => {
|
||||
view! {
|
||||
<Select
|
||||
label=label.clone()
|
||||
@@ -89,7 +97,9 @@ impl InputType {
|
||||
data_map=data_map
|
||||
required=required
|
||||
/>
|
||||
}.into_any()
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::fmt::Display;
|
||||
pub enum ValueType {
|
||||
/// Indicates a `String` value.
|
||||
String(Option<String>),
|
||||
/// Indicates an `i32` value.
|
||||
/// Indicates an `i32` value. /// Indicates an `f32` value.
|
||||
Number(Option<String>),
|
||||
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
|
||||
List(Option<Vec<ValueType>>),
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::common::ValueType;
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use leptos::html::Input;
|
||||
use leptos::logging::debug_log;
|
||||
use crate::components::{use_validation_context, InputState};
|
||||
use crate::components::validated_form::ValidationState;
|
||||
|
||||
@@ -28,6 +29,8 @@ pub enum TextFieldType {
|
||||
fn validate(required: bool, value: &str, input_type: &TextFieldType) -> ValidationState {
|
||||
if required && value.is_empty() {
|
||||
ValidationState::Invalid("This field is required.".to_string())
|
||||
} else if !required && value.is_empty() {
|
||||
ValidationState::Valid
|
||||
} else {
|
||||
match input_type {
|
||||
TextFieldType::Email(domains) => validate_email(value, domains),
|
||||
@@ -54,7 +57,7 @@ fn validate_email(input: &str, valid_domains: &Vec<String>) -> ValidationState {
|
||||
|
||||
/// Validates a number.
|
||||
fn validate_number(input: &str) -> ValidationState {
|
||||
if input.parse::<i32>().is_ok() {
|
||||
if input.parse::<f32>().is_ok() {
|
||||
ValidationState::Valid
|
||||
} else {
|
||||
ValidationState::Invalid("Value is not a valid number.".to_string())
|
||||
@@ -64,7 +67,7 @@ fn validate_number(input: &str) -> ValidationState {
|
||||
#[component]
|
||||
pub fn OutlinedTextField(
|
||||
#[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(optional, into)] input_type: Signal<TextFieldType>,
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
@@ -73,30 +76,28 @@ pub fn OutlinedTextField(
|
||||
#[prop(optional, into)] required: Signal<bool>
|
||||
) -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let data_member = StoredValue::new(data_member);
|
||||
|
||||
// Register this input's validation signal.
|
||||
let validator_context = use_validation_context()
|
||||
.expect("FormValidSignal was not found");
|
||||
|
||||
let raw_value = Signal::derive({
|
||||
let data_member = data_member.clone();
|
||||
move ||
|
||||
data_map
|
||||
.get()
|
||||
.get(&data_member)
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.to_string()
|
||||
});
|
||||
let display_value = RwSignal::new(
|
||||
data_map.get_untracked()
|
||||
.get(&data_member.get_value())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
let dirty = RwSignal::new(false);
|
||||
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 || {
|
||||
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));
|
||||
|
||||
@@ -106,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 |_| {
|
||||
// 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).
|
||||
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
|
||||
// valid types, and parses them accordingly.
|
||||
let on_input = {
|
||||
let data_member = data_member.clone();
|
||||
move |e| {
|
||||
let on_input = move |e| {
|
||||
let to_parse = event_target_value(&e);
|
||||
debug_log!("Setting value to display_value: {}", to_parse);
|
||||
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() {
|
||||
TextFieldType::Number => ValueType::Number(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
|
||||
// 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);
|
||||
map.insert(data_member.get_value(), into_map);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
@@ -147,11 +155,11 @@ pub fn OutlinedTextField(
|
||||
transition-all duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
r#type=str_type
|
||||
r#type="text"
|
||||
disabled=disabled
|
||||
placeholder=placeholder
|
||||
prop:name=name
|
||||
prop:value=raw_value
|
||||
prop:value=display_value
|
||||
on:input=on_input
|
||||
on:blur=on_blur
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,8 @@ use leptos::prelude::*;
|
||||
pub fn AdminScholarshipPage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
<div class="font-bold text-3xl">
|
||||
"Scholarships View"
|
||||
</div>
|
||||
<div>
|
||||
"This is a placeholder for now. It will show a list of all scholarships."
|
||||
</div>
|
||||
<div class="font-bold text-3xl">"Scholarships View"</div>
|
||||
<div>"This is a placeholder for now. It will show a list of all scholarships."</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,9 @@ pub fn StudentAcademicsPage() -> impl IntoView {
|
||||
let controller = use_student_form("academics");
|
||||
|
||||
view! {
|
||||
<Show
|
||||
when=move || controller.data_resource.get().is_some()
|
||||
fallback=Loading
|
||||
>
|
||||
<Suspense fallback=Loading>
|
||||
{move || controller.data_resource.get().map(|_|
|
||||
view! {
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Academic Information" on_submit=controller.submit_action>
|
||||
@@ -57,6 +56,8 @@ pub fn StudentAcademicsPage() -> impl IntoView {
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}).collect_view()
|
||||
}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,7 @@ pub fn StudentAthleticsPage() -> impl IntoView {
|
||||
let controller = use_student_form("athletics");
|
||||
|
||||
view! {
|
||||
<Show
|
||||
when=move || controller.data_resource.get().is_some()
|
||||
fallback=Loading
|
||||
>
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Athletics Information" on_submit=controller.submit_action>
|
||||
|
||||
@@ -60,53 +60,69 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
||||
let Some(Ok(student_info)) = student_resource.get() else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
|
||||
let valid_list = scholarships_list.iter()
|
||||
let valid_list = scholarships_list
|
||||
.iter()
|
||||
.filter_map(|scholarship| {
|
||||
let scholarship_name = scholarship.data.get("name")
|
||||
let scholarship_name = scholarship
|
||||
.data
|
||||
.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string().ok().flatten().unwrap_or_default();
|
||||
.as_string()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
debug_log!("Checking scholarship: {}", scholarship_name);
|
||||
|
||||
// Each scholarship will return Some(itself), or None. This is will be automatically
|
||||
// filtered based on the information. Invalid scholarships are None, along with those
|
||||
// that fail validation for any reason. Scholarships that are valid or did not choose
|
||||
// any requirements are Some(scholarship).
|
||||
let requirements_map = scholarship.data.get("requirements")
|
||||
let requirements_map = scholarship
|
||||
.data
|
||||
.get("requirements")
|
||||
.unwrap_or(&ValueType::Map(None))
|
||||
.as_map().ok().flatten()
|
||||
.as_map()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.values().cloned()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<ValueType>>();
|
||||
|
||||
let resolved_requirements = requirements_map
|
||||
.iter()
|
||||
.map(|list_val| {
|
||||
// We want to cast to a list, and then map the list to a new
|
||||
// one that contains the correct ComparisonData.
|
||||
let list = list_val.as_list().ok().flatten().unwrap_or_default();
|
||||
list.iter().filter_map(|v| {
|
||||
let id_string = v.as_string().ok().flatten().unwrap_or_default();
|
||||
relations_list.iter()
|
||||
let list = list_val
|
||||
.as_list()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
list.iter()
|
||||
.filter_map(|v| {
|
||||
let id_string = v
|
||||
.as_string()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
relations_list
|
||||
.iter()
|
||||
.find(|relation| relation.id == id_string)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<ComparisonData>>()
|
||||
})
|
||||
.collect::<Vec<Vec<ComparisonData>>>();
|
||||
|
||||
debug_log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>());
|
||||
|
||||
let valid = resolved_requirements.iter()
|
||||
debug_log!(
|
||||
"Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>()
|
||||
);
|
||||
let valid = resolved_requirements
|
||||
.iter()
|
||||
.all(|list| {
|
||||
list.iter().fold(false, |prev, requirement| {
|
||||
let result = requirement.compare(&student_info)
|
||||
list.iter()
|
||||
.fold(
|
||||
false,
|
||||
|prev, requirement| {
|
||||
let result = requirement
|
||||
.compare(&student_info)
|
||||
.unwrap_or(false);
|
||||
|
||||
if prev { prev } else { result }
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
if valid {
|
||||
debug_log!("Scholarship is valid.");
|
||||
Some(scholarship.clone())
|
||||
@@ -116,15 +132,19 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
||||
})
|
||||
.collect::<Vec<ExpandableInfo>>();
|
||||
|
||||
// Each scholarship will return Some(itself), or None. This is will be automatically
|
||||
// filtered based on the information. Invalid scholarships are None, along with those
|
||||
// that fail validation for any reason. Scholarships that are valid or did not choose
|
||||
// any requirements are Some(scholarship).
|
||||
|
||||
// We want to cast to a list, and then map the list to a new
|
||||
// one that contains the correct ComparisonData.
|
||||
|
||||
view! {
|
||||
<div>{format!("Valid scholarships: {}", valid_list.len())}</div>
|
||||
<For
|
||||
each=move || valid_list.clone()
|
||||
key=|scholarship| {
|
||||
scholarship
|
||||
.subject
|
||||
.clone()
|
||||
}
|
||||
key=|scholarship| { scholarship.subject.clone() }
|
||||
children=move |scholarship| {
|
||||
let scholarship = StoredValue::new(scholarship);
|
||||
let scholarship_name = Memo::new(move |_| {
|
||||
@@ -163,7 +183,8 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
/>
|
||||
}.into_any()
|
||||
}
|
||||
.into_any()
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn StudentShell() -> impl IntoView {
|
||||
],
|
||||
),
|
||||
),
|
||||
TabInfo::new("Eligible Scholarships", "eligibility", None)
|
||||
TabInfo::new("Eligible Scholarships", "eligibility", None),
|
||||
]
|
||||
/>
|
||||
</Authenticated>
|
||||
|
||||
Reference in new issue
Block a user