Clone in a terminal:
lib.rs
src/ - Template
// Copyright (c) 2026 Johan Mattsson // License: MIT #![doc = include_str!("../README.md")] use gloo_console::{error, log}; use wasm_bindgen::JsCast; use web_sys::{Element, HtmlDocument, HtmlElement, HtmlInputElement, NodeList}; pub type SharedComponent<T> = std::rc::Rc<std::cell::RefCell<T>>; pub mod prelude; pub struct InitFn(pub fn()); inventory::collect!(InitFn); pub struct InitComponentFn(pub fn()); inventory::collect!(InitComponentFn); pub struct UpdateComponentFn { pub component: &'static str, pub dispatch: fn(&str), } inventory::collect!(UpdateComponentFn); /// Global counter shared by all components created by the #[component] macro. /// The proc-macro-generated code will increment this to obtain a unique wasm id. pub static NEXT_COMPONENT_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1); #[derive(Debug, Clone, Copy)] pub struct Error; impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "kaja_web::Error") } } impl std::error::Error for Error {} pub type Result<T> = std::result::Result<T, Error>; pub struct CallbackRegistration { pub register: fn(), } inventory::collect!(CallbackRegistration); /// Register callbacks that can be used in JS to run Rust functions in the WASM /// module. This needs to run when the WASM bundle is loaded. /// /// ```rust,ignore /// #[wasm_bindgen(start)] /// pub fn init() { /// init_callbacks(); /// } /// ``` /// /// The Rust functions are marked with the `#[callback]` attribute in order to /// be callable from JS. The JS function in this example has the name /// `testCallback`. Note that camel case is used in JS and regular snake case /// is used in Rust for the same function. /// /// ```rust,ignore /// #[callback(testCallback)] /// pub fn test_callback() { /// } /// ``` /// /// ```rust,ignore /// let html = html!{{ /// <button onclick="testCallback()">Test</button> /// }}; /// ``` pub fn init_callbacks() { for init in inventory::iter::<InitFn> { (init.0)(); } for init in inventory::iter::<InitComponentFn> { (init.0)(); } } /// Queries the document for a single element matching the given selector. /// Returns `None` if no element is found. Returns first matching element if multiple are found. pub fn get_element(selector: &str) -> Option<HtmlElement> { let window = web_sys::window().ok_or(Error).ok()?; let document = window.document().ok_or(Error).ok()?; let elements = document .query_selector_all(selector) .map_err(|_| Error) .ok()?; if elements.length() == 0 { return None; } if elements.length() > 1 { let msg = format!( "Expecting a single element, found {} instead, for {}", elements.length(), selector ); error!(msg); } if let Some(node) = elements.item(0) { if let Ok(element) = node.dyn_into::<HtmlElement>() { return Some(element); } } None } /// Returns all elements matching the given selector. /// For example, `get_elements(".test")` would return all elements with the class `test`. pub fn get_elements(selector: &str) -> Vec<HtmlElement> { let window = web_sys::window(); if window.is_none() { return Vec::new(); } let document = window.unwrap().document(); if document.is_none() { return Vec::new(); } let elements = document.unwrap().query_selector_all(selector); if elements.is_err() { let msg = format!("Failed to query selector all: {:?}", selector); error!(msg); return Vec::new(); } return node_list_to_vec(elements.unwrap()); } /// Sets the inner HTML of the elements matching the given selector. /// Returns an error if no element is found. pub fn inner_html(selector: &str, html: &str) -> Result<()> { let window = web_sys::window().ok_or(Error)?; let document = window.document().ok_or(Error)?; let elements = document.query_selector_all(selector).map_err(|_| Error)?; if elements.length() == 0 { return Err(Error); } for i in 0..elements.length() { if let Some(node) = elements.item(i) { if let Ok(element) = node.dyn_into::<Element>() { element.set_inner_html(html); } } } Ok(()) } /// Sets the inner text of the elements matching the given selector. /// Returns an error if no element are found. pub fn inner_text(selector: &str, text: &str) -> Result<()> { let window = web_sys::window().ok_or(Error)?; let document = window.document().ok_or(Error)?; let elements = document.query_selector_all(selector).map_err(|_| Error)?; if elements.length() == 0 { return Err(Error); } for i in 0..elements.length() { if let Some(node) = elements.item(i) { if let Ok(element) = node.dyn_into::<Element>() { element.set_text_content(Some(text)); } } } Ok(()) } /// Get value from an input tag. /// ```html /// <input type="text" id="test" value="test value" /> /// ``` /// /// ```rust,ignore /// let value = get_value("#test"); /// assert_eq!(value, Some("test value")); /// ``` pub fn get_value(selector: &str) -> Option<String> { let element = get_element(selector)?; if let Ok(input) = element.dyn_into::<HtmlInputElement>() { return Some(input.value()); } None } /// Get value from input tag, or return an empty string if no value is found. pub fn try_get_value(selector: &str) -> String { let value = get_value(selector); return value.unwrap_or_default(); } /// Sets the inner HTML of elements matching the given selector, if at least one exists. pub fn try_inner_html(selector: &str, html: &str) { let _ = inner_html(selector, html); } /// Sets the inner text of elements matching the given selector, if at least one exists. pub fn try_inner_text(selector: &str, text: &str) { let _ = inner_text(selector, text); } fn node_list_to_vec(list: NodeList) -> Vec<HtmlElement> { (0..list.length()) .filter_map(|i| list.item(i)) .filter_map(|node| node.dyn_into::<HtmlElement>().ok()) .collect() } /// Get a value set in any cookie. pub fn get_cookie_value(name: &str) -> Option<String> { let window = web_sys::window().ok_or(Error).ok()?; let document = window.document().ok_or(Error).ok()?; let cookies = if let Ok(html_doc) = document.dyn_into::<HtmlDocument>() { html_doc.cookie().unwrap_or_default() } else { String::new() }; for cookie in cookies.split(';') { let cookie = cookie.trim(); if let Some((key, value)) = cookie.split_once('=') { if key == name { return Some(value.to_string()); } } } None } /// Set a cookie value. pub fn set_cookie_value(name: &str, value: &str) -> Result<()> { let window = web_sys::window().ok_or(Error)?; let document = window.document().ok_or(Error)?; const MAX_AGE: u64 = 60 * 60 * 24 * 365 * 10; // in seconds if let Ok(html_doc) = document.dyn_into::<HtmlDocument>() { let cookie = format!("{}={}; path=/; Max-Age={}", name, value, MAX_AGE); html_doc.set_cookie(&cookie).map_err(|_| Error)?; return Ok(()); } Err(Error) } /// Set a session cookie, it will be deleted when the browser is closed. pub fn set_session_cookie_value(name: &str, value: &str) -> Result<()> { let window = web_sys::window().ok_or(Error)?; let document = window.document().ok_or(Error)?; if let Ok(html_doc) = document.dyn_into::<HtmlDocument>() { let cookie = format!("{}={}; path=/", name, value); html_doc.set_cookie(&cookie).map_err(|_| Error)?; return Ok(()); } Err(Error) } /// A callback registerd with `#[callback(someFoo)]` can be retrieved via /// `get_callback("someFoo")`. This is usefull for assigning the callback /// to HTML elements without generating an HTML string. /// /// Example: /// ```rust,ignore /// use web_sys::HtmlImageElement; /// use gloo_console::log; /// /// #[callback(imageLoaded)] /// pub fn image_loaded_callback(event: web_sys::Event) { /// log!("Image loaded. Processing it ..."); /// } /// /// fn create_image() { /// let window = web_sys::window().unwrap(); /// let document = window.document().unwrap(); /// /// let img = document /// .create_element("img") /// .unwrap() /// .dyn_into::<HtmlImageElement>() /// .unwrap(); /// /// let onload_callback = get_callback("imageLoaded"); /// img.set_onload(onload_callback.as_ref()); /// /// img.set_src(&blob_url); /// } /// ``` pub fn get_callback(name: &str) -> Option<js_sys::Function> { let window = web_sys::window()?; if let Ok(val) = js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str(name)) { if let Ok(func) = val.dyn_into::<js_sys::Function>() { return Some(func); } else { log!( "Not a callback function: ", name, "Did you run init_callbacks()?" ); } } None } /// Generated component handle API. See kaja-web-component. pub trait ComponentHandle { type State; fn set_attribute<T>(&self, name: &str, value: T) -> Result<()> where T: ToString; fn set_attribute_string(&self, name: &str, value: &str) -> Result<()>; fn get_attribute_string(&self, name: &str) -> Option<String>; fn get_attribute<T>(&self, name: &str) -> Option<T> where T: std::str::FromStr; fn get_attributes(&self) -> std::rc::Rc<std::cell::RefCell<Self::State>>; fn borrow(&self) -> std::cell::Ref<'_, Self::State>; fn borrow_mut(&self) -> std::cell::RefMut<'_, Self::State>; fn with_mut<F>(&self, f: F) where F: FnOnce(&mut Self::State); fn with<F>(&self, f: F) where F: FnOnce(&Self::State); fn get_id(&self) -> String; } /// HTML Component. See kaja-web-component. pub trait Component: Default + ComponentHandle { fn connected(&self) {} fn disconnected(&self) {} fn attribute_changed(&self, name: &str, _old_value: &str, _new_value: &str) { log!("Attribute changed but no update handler for ", name); } } /// Get HTML element by ID pub fn get_element_by_id(id: &str) -> Option<HtmlElement> { let window = web_sys::window().ok_or(Error).ok()?; let document = window.document().ok_or(Error).ok()?; let element = document.get_element_by_id(id)?; let html_element = element.dyn_into::<HtmlElement>().ok()?; Some(html_element) } /// Drop CSS class from CSS class list pub fn remove_css_class(element: &HtmlElement, css_class: &str) { use js_sys::Array; use wasm_bindgen::JsValue; let token = { let arr = Array::new(); arr.push(&JsValue::from_str(css_class)); arr }; let _ = element.class_list().remove(&token); } /// Add CSS class to CSS class list pub fn add_css_class(element: &HtmlElement, css_class: &str) { use js_sys::Array; use wasm_bindgen::JsValue; let token = { let arr = Array::new(); arr.push(&JsValue::from_str(css_class)); arr }; let _ = element.class_list().add(&token); }

File history (47 commits)

c9e8f6d Johan Mattsson - johan.mattsson.m@gmail.com
Template
Template
8a643c4 Johan Mattsson - johan.mattsson.m@gmail.com
More cleanup
More cleanup
16db169 Johan Mattsson - johan.mattsson.m@gmail.com
Cleanup
Cleanup
e5c0d89 Johan Mattsson - johan.mattsson.m@gmail.com
NPM package
NPM package
ae58226 Johan Mattsson - johan.mattsson.m@gmail.com
Update example
Update example
10d33e7 Johan Mattsson - johan.mattsson.m@gmail.com
Move compoent code to macro and test
Move compoent code to macro and test
6680450 Johan Mattsson - johan.mattsson.m@gmail.com
Restructure
Restructure
84a11d8 Johan Mattsson - johan.mattsson.m@gmail.com
Attribute tests
Attribute tests
6136236 Johan Mattsson - johan.mattsson.m@gmail.com
Tests and examples
Tests and examples
321ec6b Johan Mattsson - johan.mattsson.m@gmail.com
Remove threads with shared memory
Remove threads with shared memory
9c53342 Johan Mattsson - johan.mattsson.m@gmail.com
Web worker test
Web worker test
45ddf9d Johan Mattsson - johan.mattsson.m@gmail.com
ArcSwap instead of lock
ArcSwap instead of lock
2759560 Johan Mattsson - johan.mattsson.m@gmail.com
Multiple wasm threads
Multiple wasm threads
cfe5d0c Johan Mattsson - johan.mattsson.m@gmail.com
Update component code
Update component code
8def408 Johan Mattsson - johan.mattsson.m@gmail.com
New example component
New example component
06a68b2 Johan Mattsson - johan.mattsson.m@gmail.com
Try multi thread component ideas
Try multi thread component ideas
5b0444b Johan Mattsson - johan.mattsson.m@gmail.com
Revert "Add and remove css classes"
Revert "Add and remove css classes"

This reverts commit 2226f52c8addbbf364bf019ff2dfbef8133c4a25.
2226f52 Johan Mattsson - johan.mattsson.m@gmail.com
Add and remove css classes
Add and remove css classes
e6c152a Johan Mattsson - johan.mattsson.m@gmail.com
Rerender
Rerender
d9014e7 Johan Mattsson - johan.mattsson.m@gmail.com
Get and set macro for component
Get and set macro for component
0cd2174 Johan Mattsson - johan.mattsson.m@gmail.com
Update function for components referenced by id
Update function for components referenced by id
6b30cee Johan Mattsson - johan.mattsson.m@gmail.com
Callback updated
Callback updated
db8e1c7 Johan Mattsson - johan.mattsson.m@gmail.com
Make attributes &str
Make attributes &str
674bd7b Johan Mattsson - johan.mattsson.m@gmail.com
Full example using web components
Full example using web components

Click counter example.
dd9e29d Johan Mattsson - johan.mattsson.m@gmail.com
Get component from static storage
Get component from static storage
48106f6 Johan Mattsson - johan.mattsson.m@gmail.com
One global store per component
One global store per component
4693249 Johan Mattsson - johan.mattsson.m@gmail.com
Update component id
Update component id
00f049d Johan Mattsson - johan.mattsson.m@gmail.com
Get html elemennt for id
Get html elemennt for id
3521197 Johan Mattsson - johan.mattsson.m@gmail.com
Macro update
Macro update
210d23a Johan Mattsson - johan.mattsson.m@gmail.com
Update init
Update init
eff2a45 Johan Mattsson - johan.mattsson.m@gmail.com
Component update
Component update
c8b0c25 Johan Mattsson - johan.mattsson.m@gmail.com
Component sketch
Component sketch
3847150 Johan Mattsson - johan.mattsson.m@gmail.com
Drop sync
Drop sync
de855d3 Johan Mattsson - johan.mattsson.m@gmail.com
License
License
9b5cae2 Johan Mattsson - johan.mattsson.m@gmail.com
Meta data etc.
Meta data etc.
3ac64e7 Johan Mattsson - johan.mattsson.m@gmail.com
Better example for get_callback
Better example for get_callback
023e4be Johan Mattsson - johan.mattsson.m@gmail.com
Add a function for getting a referense to a callback function
Add a function for getting a referense to a callback function
c7ded90 Johan Mattsson - johan.mattsson.m@gmail.com
Set cookie value
Set cookie value
3a11c28 Johan Mattsson - johan.mattsson.m@gmail.com
More documentation
More documentation
f478ff8 Johan Mattsson - johan.mattsson.m@gmail.com
Clean up README.md and examples
Clean up README.md and examples
67885fc Johan Mattsson - johan.mattsson.m@gmail.com
Cookie and input value
Cookie and input value

Some helpers.
3f8adb6 Johan Mattsson - johan.mattsson.m@gmail.com
Cleanup and documentation
Cleanup and documentation
72f0126 Johan Mattsson - johan.mattsson.m@gmail.com
Prelude for imports
Prelude for imports
c043f7c Johan Mattsson - johan.mattsson.m@gmail.com
Remove old macro
Remove old macro
aa095dd Johan Mattsson - johan.mattsson.m@gmail.com
Move callback init
Move callback init
bf5df00 Johan Mattsson - johan.mattsson.m@gmail.com
Clock counter example
Clock counter example
b42e691 Johan Mattsson - johan.mattsson.m@gmail.com
Some DOM selector helpers
Some DOM selector helpers

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112