1
//! HTMX event handlers for form validation and error handling.
2
//!
3
//! This module provides WASM-based handlers for HTMX events, replacing
4
//! the JavaScript `htmx-validate.js` script.
5

            
6
use wasm_bindgen::JsCast;
7
use wasm_bindgen::prelude::*;
8
use web_sys::Element;
9

            
10
/// Sets up the HTMX beforeSwap event listener for form validation.
11
///
12
/// This handler:
13
/// - Allows 4xx responses to be swapped in
14
/// - Parses JSON error responses and formats them as HTML
15
/// - Redirects error content to the result-box element
16
/// - Manages CSS classes for success/error styling
17
pub fn setup_htmx_validation() {
18
    let Some(document) = web_sys::window().and_then(|w| w.document()) else {
19
        return;
20
    };
21

            
22
    let callback =
23
        Closure::wrap(Box::new(handle_before_swap) as Box<dyn FnMut(web_sys::CustomEvent)>);
24

            
25
    let _ = document
26
        .add_event_listener_with_callback("htmx:beforeSwap", callback.as_ref().unchecked_ref());
27
    callback.forget();
28
}
29

            
30
fn handle_before_swap(evt: web_sys::CustomEvent) {
31
    let Some(document) = web_sys::window().and_then(|w| w.document()) else {
32
        return;
33
    };
34

            
35
    let detail = evt.detail();
36
    let Some(detail_obj) = detail.dyn_ref::<js_sys::Object>() else {
37
        return;
38
    };
39

            
40
    let xhr_val = match js_sys::Reflect::get(detail_obj, &"xhr".into()) {
41
        Ok(v) => v,
42
        Err(_) => return,
43
    };
44
    let Some(xhr) = xhr_val.dyn_ref::<web_sys::XmlHttpRequest>() else {
45
        return;
46
    };
47

            
48
    let status = xhr.status().unwrap_or(0);
49
    let result_box = document.get_element_by_id("result-box");
50

            
51
    if (400..500).contains(&status) {
52
        handle_client_error(detail_obj, xhr, result_box.as_ref());
53
    } else if (200..300).contains(&status) {
54
        handle_success(result_box.as_ref());
55
    }
56
}
57

            
58
fn handle_client_error(
59
    detail: &js_sys::Object,
60
    xhr: &web_sys::XmlHttpRequest,
61
    result_box: Option<&Element>,
62
) {
63
    // Allow the error content to be swapped
64
    let _ = js_sys::Reflect::set(detail, &"shouldSwap".into(), &true.into());
65

            
66
    // Check if response is JSON
67
    let content_type = xhr
68
        .get_response_header("Content-Type")
69
        .ok()
70
        .flatten()
71
        .unwrap_or_default();
72

            
73
    if content_type.contains("application/json")
74
        && let Ok(Some(text)) = xhr.response_text()
75
    {
76
        let error_html = parse_json_error(&text);
77
        let _ = js_sys::Reflect::set(detail, &"serverResponse".into(), &error_html.into());
78
    }
79

            
80
    // Redirect to result-box and update styling
81
    if let Some(box_el) = result_box {
82
        let _ = js_sys::Reflect::set(detail, &"target".into(), box_el);
83
        let _ = box_el.class_list().remove_1("valid-message");
84
        let _ = box_el.class_list().add_1("error-message");
85
    }
86
}
87

            
88
fn handle_success(result_box: Option<&Element>) {
89
    if let Some(box_el) = result_box {
90
        let _ = box_el.class_list().remove_1("error-message");
91
        let _ = box_el.class_list().add_1("valid-message");
92
    }
93
}
94

            
95
fn parse_json_error(text: &str) -> String {
96
    match serde_json::from_str::<serde_json::Value>(text) {
97
        Ok(json) => {
98
            let status = json
99
                .get("status")
100
                .and_then(|v| v.as_str())
101
                .unwrap_or("An error occurred");
102
            let message = json.get("message").and_then(|v| v.as_str()).unwrap_or("");
103
            format!(r#"<div class="error-alert">{status}: {message}</div>"#)
104
        }
105
        Err(_) => "Unparseable response received".to_string(),
106
    }
107
}