Lines
0 %
Functions
Branches
100 %
//! HTMX event handlers for form validation and error handling.
//!
//! This module provides WASM-based handlers for HTMX events, replacing
//! the JavaScript `htmx-validate.js` script.
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::Element;
/// Sets up the HTMX beforeSwap event listener for form validation.
///
/// This handler:
/// - Allows 4xx responses to be swapped in
/// - Parses JSON error responses and formats them as HTML
/// - Redirects error content to the result-box element
/// - Manages CSS classes for success/error styling
pub fn setup_htmx_validation() {
let Some(document) = web_sys::window().and_then(|w| w.document()) else {
return;
};
let callback =
Closure::wrap(Box::new(handle_before_swap) as Box<dyn FnMut(web_sys::CustomEvent)>);
let _ = document
.add_event_listener_with_callback("htmx:beforeSwap", callback.as_ref().unchecked_ref());
callback.forget();
}
fn handle_before_swap(evt: web_sys::CustomEvent) {
let detail = evt.detail();
let Some(detail_obj) = detail.dyn_ref::<js_sys::Object>() else {
let xhr_val = match js_sys::Reflect::get(detail_obj, &"xhr".into()) {
Ok(v) => v,
Err(_) => return,
let Some(xhr) = xhr_val.dyn_ref::<web_sys::XmlHttpRequest>() else {
let status = xhr.status().unwrap_or(0);
let result_box = document.get_element_by_id("result-box");
if (400..500).contains(&status) {
handle_client_error(detail_obj, xhr, result_box.as_ref());
} else if (200..300).contains(&status) {
handle_success(result_box.as_ref());
fn handle_client_error(
detail: &js_sys::Object,
xhr: &web_sys::XmlHttpRequest,
result_box: Option<&Element>,
) {
// Allow the error content to be swapped
let _ = js_sys::Reflect::set(detail, &"shouldSwap".into(), &true.into());
// Check if response is JSON
let content_type = xhr
.get_response_header("Content-Type")
.ok()
.flatten()
.unwrap_or_default();
if content_type.contains("application/json")
&& let Ok(Some(text)) = xhr.response_text()
{
let error_html = parse_json_error(&text);
let _ = js_sys::Reflect::set(detail, &"serverResponse".into(), &error_html.into());
// Redirect to result-box and update styling
if let Some(box_el) = result_box {
let _ = js_sys::Reflect::set(detail, &"target".into(), box_el);
let _ = box_el.class_list().remove_1("valid-message");
let _ = box_el.class_list().add_1("error-message");
fn handle_success(result_box: Option<&Element>) {
let _ = box_el.class_list().remove_1("error-message");
let _ = box_el.class_list().add_1("valid-message");
fn parse_json_error(text: &str) -> String {
match serde_json::from_str::<serde_json::Value>(text) {
Ok(json) => {
let status = json
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("An error occurred");
let message = json.get("message").and_then(|v| v.as_str()).unwrap_or("");
format!(r#"<div class="error-alert">{status}: {message}</div>"#)
Err(_) => "Unparseable response received".to_string(),