Kaja Web
Rust framework for web development targeting WebAssembly
The Core Concept
The programmer knows when a part of the page needs to be updated, no need for
code that re-renders things automatically. HTML is treated as strings and
the programmer sets the content of a component explicitly.
That is the foundation for web applications written in Rust using Kaja Web.
Demo
There are more examples in the examples folder in the repo.
Here is one on the server: clickcounter.
Quick Start from Template
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
cargo install cargo-generate
cargo generate --git https://git.kajacode.com/kaja-web kaja-web-template
The Crates
The framework is split into four crates:
- kaja-html-macro – provides a way to create HTML code in the Rust source files.
- kaja-web – includes functions for managing the frontend.
- kaja-callback-macro – provides an annotation for Rust functions that will be called from JS handlers.
- kaja-web-component – provides support for using Kaja Web applications as Web Components.
Getting Started
Install the required dependencies:
cargo add kaja-web
cargo add kaja-html-macro
cargo add kaja-callback-macro
cargo add kaja-web-component
cargo add wasm-bindgen
cargo add inventory
cargo add web-sys
cargo add gloo
cargo add js-sys
cargo add console_error_panic_hook
Note that you need to specify features when you add web-sys, most features are disabled by default.
Write the Code
Probably in src/lib.rs
use kaja_web::prelude::*;
struct AppData {
current_count: u32,
}
pub static APP_DATA: RwLock<AppData> = RwLock::new(
AppData { current_count: 0 }
);
fn create_counter_display(count: u32) {
let counter = html! {{
<div class="click-counter-display">$count</div>
<button class="click-counter-increment-button"
onclick="incrementCounter()">
Increment
</button>
}};
try_inner_html("#click-counter", counter.as_str());
}
fn update_count(app_data: &AppData) {
let count = app_data.current_count;
let updated = count.to_string().as_str();
let result = inner_text(".click-counter-display", updated);
if result.is_err() {
log!(".click-counter-display not found");
}
}
#[callback(incrementCounter)]
pub fn increment_counter() {
let mut app_data = APP_DATA.write().unwrap();
let updated = app_data.current_count.saturating_add(1);
app_data.current_count = updated;
update_count(&app_data);
}
#[wasm_bindgen(start)]
pub fn init() {
init_callbacks();
create_counter_display(0);
}
Create the HTML Page
index.html
<script
type="module"
src="clickcounter.js" defer>
</script>
<body>
<main id="click-counter"></main>
</body>
Build the Project
wasm-pack build \
--release \
--target web \
--out-dir pkg \
--out-name clickcounter \
--no-typescript
cp index.html pkg/
Change --release to --debug
if you want good symbols in the panic handler.
Debugging
Enable panic handler if the current build is a debug build. Compile with
--debug and add this to your code:
#[wasm_bindgen(start)]
pub fn init() {
std::panic::set_hook(
Box::new(console_error_panic_hook::hook),
);
console_error_panic_hook::set_once();
init_callbacks();
}
Locking and Sharing State
There are several ways to store and share the state of a WASM
program in Rust. One option is using an Rc<RefCell<T>>
to hold your object; another is using an RwLock. I prefer RwLock.
When the JS engine calls your Rust code, take a read or write lock
on your AppData object in the callback function and pass references
to it in other functions (& or &mut).
The rule to follow is: lock the app state object in callback
functions and pass references around in all non-callback functions
that need the data.
use kaja_web::prelude::*;
use std::sync::RwLock;
struct AppData {
current_count: i32,
}
pub static APP_DATA: RwLock = RwLock::new(
AppData {
current_count: 0
}
);
fn other_function(app: &mut AppData) {
app.update_view();
}
#[callback(updateCallbackFunction)]
pub fn update_callback_function() {
let mut app_data = APP_DATA.write().unwrap();
other_function(&mut app_data);
}
Web Components
You can build reusable HTML elements with Kaja and Rust, similar to React
and other front-end frameworks, while remaining small and fast. Components
can be combined with ordinary HTML, CSS, and JavaScript; the whole application
does not need to follow one component architecture.
Full example:
main-component.
Node example:
Node Module.
Reusability
For example, components can be combined like this:
<main-component>
<h1>Hello!</h1>
<counter-component></counter-component>
<timer-component></timer-component>
</main-component>
Component
use kaja_web::prelude::*;
#[component("main-component")]
struct Main {
#[attribute]
test: bool,
}
The #[component] macro defines a custom HTML element,
named main-component in this example. The
#[attribute] macro associates the test
field with an HTML attribute.
Attributes can be updated with JavaScript:
document
.querySelector("main-component")
.setAttribute("test", "true");
They can also be set in HTML:
<main-component test="true"></main-component>
Or in Rust:
for element in get_elements("main-component") {
let _ = element.set_attribute("test", "true");
}
Implementation
The macro generates an empty implementation type named
MainComponent. It is distinct from Main
but has access to the same component functionality.
use kaja_web::prelude::*;
impl Component for MainComponent {
fn connected(&self) {
log!("Connected:", self.get_id());
}
fn disconnected(&self) {
log!("Disconnected:", self.get_id());
}
fn attribute_changed(
&self,
name: &str,
_old_value: &str,
_new_value: &str
) {
if name == "test" {
self.render();
}
}
}
Rendering
Rendering can happen in attribute_changed, but it is
a separate function in this example.
use kaja_web::prelude::*;
impl MainComponent {
fn render(&self) {
let test: bool = self.get_attribute("test").unwrap_or_default();
let content = html! {{
<p>Test: $test</p>
}};
let id = self.get_id();
if let Some(element) = get_element_by_id(&id) {
element.set_inner_html(content.as_str());
} else {
error!("Element not found for component id:", id);
}
}
}
Startup
use kaja_web::prelude::*;
#[wasm_bindgen(start)]
pub fn init() {
let elements = get_elements("main-component");
for element in elements {
let id = element.id();
if id.is_empty() {
continue;
}
if let Some(element) = get_element_by_id(&id) {
let test = true;
let _ = element.set_attribute("test", test.to_string().as_str());
}
}
}
Usage
<main-component test="false"></main-component>
Benchmarks
Benchmarks for the size of binaries and speed when components are used comparing popular Rust Frameworks including Kaja Web:
benchmarks.html