commit c9dc4572cbcd2a88ca5db812f03e53cbae66520c
parent e226b1870fb65e1a3a1e182149d04830eb546239
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 08:01:44 +0000
app-lib: add dom helpers
- add view_effect and el_id helpers
- add dom error handling and unit tests
- re-export dom helpers from app lib
- enable dom token list and node list features
Diffstat:
3 files changed, 95 insertions(+), 0 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -33,6 +33,7 @@ web-sys = { version = "0.3.77", features = [
"Window",
"Document",
"Element",
+ "DomTokenList",
"DomException",
"DomStringList",
"Event",
@@ -58,6 +59,7 @@ web-sys = { version = "0.3.77", features = [
"Notification",
"NotificationOptions",
"NotificationPermission",
+ "NodeList",
"PermissionDescriptor",
"PermissionName",
"PermissionState",
diff --git a/crates/app-lib/src/dom.rs b/crates/app-lib/src/dom.rs
@@ -0,0 +1,91 @@
+#![forbid(unsafe_code)]
+
+use std::fmt;
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::JsCast;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DomError {
+ WindowUnavailable,
+ DocumentUnavailable,
+ QueryFailure,
+ ClassListFailure,
+}
+
+impl fmt::Display for DomError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ DomError::WindowUnavailable => f.write_str("error.app.dom.window_unavailable"),
+ DomError::DocumentUnavailable => f.write_str("error.app.dom.document_unavailable"),
+ DomError::QueryFailure => f.write_str("error.app.dom.query_failure"),
+ DomError::ClassListFailure => f.write_str("error.app.dom.class_list_failure"),
+ }
+ }
+}
+
+impl std::error::Error for DomError {}
+
+pub fn view_effect(view: &str) -> Result<(), DomError> {
+ #[cfg(target_arch = "wasm32")]
+ {
+ let window = web_sys::window().ok_or(DomError::WindowUnavailable)?;
+ let document = window.document().ok_or(DomError::DocumentUnavailable)?;
+ let nodes = document
+ .query_selector_all("[data-view]")
+ .map_err(|_| DomError::QueryFailure)?;
+ for idx in 0..nodes.length() {
+ let Some(node) = nodes.get(idx) else {
+ continue;
+ };
+ let element: web_sys::Element = node.unchecked_into();
+ let attr = element.get_attribute("data-view").unwrap_or_default();
+ let class_list = element.class_list();
+ if attr != view {
+ class_list
+ .add_1("hidden")
+ .map_err(|_| DomError::ClassListFailure)?;
+ } else {
+ class_list
+ .remove_1("hidden")
+ .map_err(|_| DomError::ClassListFailure)?;
+ }
+ }
+ Ok(())
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let _ = view;
+ Err(DomError::WindowUnavailable)
+ }
+}
+
+pub fn el_id(id: &str) -> Option<web_sys::Element> {
+ #[cfg(target_arch = "wasm32")]
+ {
+ let window = web_sys::window()?;
+ let document = window.document()?;
+ document.get_element_by_id(id)
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let _ = id;
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{el_id, view_effect, DomError};
+
+ #[test]
+ fn view_effect_errors_on_non_wasm() {
+ let err = view_effect("home").expect_err("non-wasm");
+ assert_eq!(err, DomError::WindowUnavailable);
+ }
+
+ #[test]
+ fn el_id_returns_none_on_non_wasm() {
+ assert!(el_id("missing").is_none());
+ }
+}
diff --git a/crates/app-lib/src/lib.rs b/crates/app-lib/src/lib.rs
@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
pub mod browser;
+pub mod dom;
pub mod fetch;
pub mod geo;
pub mod locale;
@@ -12,6 +13,7 @@ pub mod symbols;
pub mod theme;
pub use browser::{browser_platform, BrowserPlatformInfo};
+pub use dom::{el_id, view_effect, DomError};
pub use fetch::{fetch_json, FetchJsonError, FetchJsonErrorKind, FetchJsonResult};
pub use geo::{geop_init, geop_is_valid, AppGeolocationPoint};
pub use locale::{get_locale, resolve_locale};