app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
MCargo.toml | 2++
Acrates/app-lib/src/dom.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/app-lib/src/lib.rs | 2++
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};