// 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)
Template
Template
More cleanup
More cleanup
Cleanup
Cleanup
NPM package
NPM package
Update example
Update example
Move compoent code to macro and test
Move compoent code to macro and test
Restructure
Restructure
Attribute tests
Attribute tests
Tests and examples
Tests and examples
Remove threads with shared memory
Remove threads with shared memory
Web worker test
Web worker test
ArcSwap instead of lock
ArcSwap instead of lock
Multiple wasm threads
Multiple wasm threads
Update component code
Update component code
New example component
New example component
Try multi thread component ideas
Try multi thread component ideas
Revert "Add and remove css classes"
Revert "Add and remove css classes"
This reverts commit 2226f52c8addbbf364bf019ff2dfbef8133c4a25.
Add and remove css classes
Add and remove css classes
Rerender
Rerender
Get and set macro for component
Get and set macro for component
Update function for components referenced by id
Update function for components referenced by id
Callback updated
Callback updated
Make attributes &str
Make attributes &str
Full example using web components
Full example using web components
Click counter example.
Get component from static storage
Get component from static storage
One global store per component
One global store per component
Update component id
Update component id
Get html elemennt for id
Get html elemennt for id
Macro update
Macro update
Update init
Update init
Component update
Component update
Component sketch
Component sketch
Drop sync
Drop sync
License
License
Meta data etc.
Meta data etc.
Better example for get_callback
Better example for get_callback
Add a function for getting a referense to a callback function
Add a function for getting a referense to a callback function
Set cookie value
Set cookie value
More documentation
More documentation
Clean up README.md and examples
Clean up README.md and examples
Cookie and input value
Cookie and input value
Some helpers.
Cleanup and documentation
Cleanup and documentation
Prelude for imports
Prelude for imports
Remove old macro
Remove old macro
Move callback init
Move callback init
Clock counter example
Clock counter example
Some DOM selector helpers
Some DOM selector helpers