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

See the lib.rs source file

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:

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

Contact

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