Lines
11.28 %
Functions
6.82 %
Branches
100 %
//! Validation logic for transaction forms.
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::{Element, HtmlInputElement};
/// Sets up field validation handlers.
pub fn setup_field_validation() {
setup_blur_validation();
setup_input_clear_validation();
}
fn setup_blur_validation() {
let Some(document) = web_sys::window().and_then(|w| w.document()) else {
return;
};
let callback = Closure::wrap(Box::new(move |event: web_sys::FocusEvent| {
let Some(target) = event.target() else {
let Ok(input) = target.dyn_into::<HtmlInputElement>() else {
let Some(field_id) = input.get_attribute("data-field") else {
if matches!(
field_id.as_str(),
"amount"
| "amount-converted"
| "from-account"
| "to-account"
| "from-commodity"
| "to-commodity"
) {
validate_field(&input, &field_id);
}) as Box<dyn FnMut(_)>);
let _ = document.add_event_listener_with_callback_and_bool(
"focusout",
callback.as_ref().unchecked_ref(),
true,
);
callback.forget();
fn setup_input_clear_validation() {
let callback = Closure::wrap(Box::new(move |event: web_sys::InputEvent| {
"from-account" | "to-account" | "from-commodity" | "to-commodity"
clear_validation_message(&input);
"input",
fn validate_field(input: &HtmlInputElement, field_id: &str) {
let Some(split_entry) = input.closest(".split-entry").ok().flatten() else {
let feedback = split_entry
.query_selector(".split-validation")
.ok()
.flatten();
if let Some(ref fb) = feedback {
fb.set_text_content(Some(""));
match field_id {
"amount" | "amount-converted" => {
let value = input.value();
if let Err(msg) = validate_amount(&value)
&& let Some(fb) = feedback
{
fb.set_text_content(Some(&msg));
"from-account" | "to-account" | "from-commodity" | "to-commodity" => {
if let Err(msg) = validate_autocomplete_field(&split_entry, input, field_id)
_ => {}
fn clear_validation_message(input: &HtmlInputElement) {
if let Some(feedback) = split_entry
.flatten()
feedback.set_text_content(Some(""));
pub fn validate_amount(value: &str) -> Result<(), String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(());
let num: f64 = trimmed
.parse()
.map_err(|_| "Invalid number format".to_string())?;
if num <= 0.0 {
return Err("Amount must be a positive number".to_string());
Ok(())
fn validate_autocomplete_field(
split_entry: &Element,
display_input: &HtmlInputElement,
field_id: &str,
) -> Result<(), String> {
let suggestions_visible = split_entry
.query_selector(".autocomplete-results")
.and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok())
.is_some_and(|el| el.style().get_property_value("display").unwrap_or_default() == "block");
if suggestions_visible {
let display_value = display_input.value();
if display_value.trim().is_empty() {
let class_name = display_input.class_name();
let value_class = if class_name.contains("account") {
"account-value"
} else {
"commodity-value"
let hidden_input = split_entry
.query_selector(&format!(r#".{value_class}[data-field="{field_id}"]"#))
.and_then(|el| el.dyn_into::<HtmlInputElement>().ok());
let hidden_value = hidden_input.map(|i| i.value()).unwrap_or_default();
if hidden_value.is_empty() {
let field_name = field_id.replace('-', " ");
return Err(format!("Please select a valid {field_name} from the list"));
pub fn validate_form() -> bool {
return false;
let mut is_valid = true;
if let Some(result_box) = document.get_element_by_id("result-box") {
result_box.set_inner_html("");
let Ok(feedbacks) = document.query_selector_all(".validation-feedback") else {
for i in 0..feedbacks.length() {
if let Some(node) = feedbacks.get(i)
&& let Ok(el) = node.dyn_into::<Element>()
el.set_text_content(Some(""));
let Ok(splits) = document.query_selector_all(".split-entry") else {
for i in 0..splits.length() {
let Some(node) = splits.get(i) else {
continue;
let Ok(split) = node.dyn_into::<Element>() else {
if !validate_split(&split) {
is_valid = false;
if !is_valid && let Some(result_box) = document.get_element_by_id("result-box") {
result_box.set_inner_html(
r#"<div class="error">Please correct the errors before submitting</div>"#,
is_valid
fn validate_split(split: &Element) -> bool {
let feedback = split.query_selector(".split-validation").ok().flatten();
let amount = get_input_value(split, r#"input[data-field="amount"]"#);
if let Some(value) = amount {
if validate_amount(&value).is_err() {
set_feedback(
&feedback,
"Amount is required and must be a positive number",
set_feedback(&feedback, "Amount is required");
let from_account = get_hidden_value(split, "from-account");
if from_account.is_none()
|| from_account
.as_ref()
.is_none_or(std::string::String::is_empty)
set_feedback(&feedback, "From account is required");
let to_account = get_hidden_value(split, "to-account");
if to_account.is_none()
|| to_account
set_feedback(&feedback, "To account is required");
let from_commodity = get_hidden_value(split, "from-commodity");
if from_commodity.is_none()
|| from_commodity
set_feedback(&feedback, "From commodity is required");
let to_commodity = get_hidden_value(split, "to-commodity");
if to_commodity.is_none()
|| to_commodity
set_feedback(&feedback, "To commodity is required");
fn get_input_value(parent: &Element, selector: &str) -> Option<String> {
parent
.query_selector(selector)
.ok()?
.and_then(|el| el.dyn_into::<HtmlInputElement>().ok())
.map(|input| input.value())
.filter(|v| !v.is_empty())
fn get_hidden_value(parent: &Element, field: &str) -> Option<String> {
let selector = format!(r#"input[type="hidden"][data-field="{field}"]"#);
.query_selector(&selector)
fn set_feedback(feedback: &Option<Element>, message: &str) {
if let Some(fb) = feedback {
fb.set_text_content(Some(message));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_amount_accepts_positive_numbers() {
assert!(validate_amount("100").is_ok());
assert!(validate_amount("100.50").is_ok());
assert!(validate_amount("0.01").is_ok());
fn validate_amount_rejects_non_positive() {
assert!(validate_amount("0").is_err());
assert!(validate_amount("-10").is_err());
fn validate_amount_rejects_invalid_format() {
assert!(validate_amount("abc").is_err());
assert!(validate_amount("10.5.5").is_err());
fn validate_amount_accepts_empty() {
assert!(validate_amount("").is_ok());
assert!(validate_amount(" ").is_ok());