Develop Web Components with rustwasm WebAssembly

I have previously written about using Emscripten to develop W3C Web Components with WebAssembly. The process involves the use of JavaScript to define a Web Component, which in turn loads a WebAssembly module to perform the necessary tasks.

In this article we are going to illustrate how to create a W3C Web Component with rustwasm WebAssembly "natively". In case you have not used the Rust programming language before, the overview below should get you up to speed quickly.

Overview

Rust is a programming language that empowers everyone to build reliable and efficient software. An important characteristic is its rich type system and ownership memory-safety, enabling it to not require a garbage collector. This results in Rust applications to be blazingly fast, memory-efficient and have no unpredictable garbage collection pauses.

rustwasm is an open source project by Mozilla that enables you to write Rust programs and compile it to WebAssembly (wasm). Without requiring a runtime or garbage collector, WebAssembly (wasm) modules written in rustwasm is small and contains no extra bloat.

wasm-bindgen is part of rustwasm that is used for facilitating high-level interactions between wasm modules and JavaScript. You can use it to import JavaScript functions/Web APIs for use in Rust or export WASM functions defined in Rust for use in JavaScript. This interoperability is useful in ensuring that a rustwasm program have access to all the available APIs of the browser and existing JavaScript applications can flexibly plug in a wasm module and interact with it.

How to develop a Web Component with rustwasm?

Prerequisites
  • Rust is installed - See https://www.rust-lang.org/tools/install
  • Python3 is installed - For use as a development HTTP server. This is not required if you are using other development web server.

1. In our Linux, Windows or Mac Terminal, enter the following command to install wasm-bindgen.

cargo install wasm-bindgen-cli

2. Next, create a new rustwasm (Rust WebAssembly) project with the following command.

cargo new webassembly_webcomponent --lib

You should see the following message.


        Created library `webassembly_webcomponent` package
        A new "webassembly_webcomponent" rustwasm package has been created. 
    

3. Using a text editor, open "Cargo.toml", and see that it contains the following.


[package]
name = "webassembly_webcomponent"
version = "0.1.0"
authors = ["djembe-waka "]
edition = "2018"

[dependencies]
            

Change the contents to the following.


[package]
name = "webassembly_webcomponent"
version = "0.1.0"
authors = ["djembe-waka "]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
js-sys = "0.3.4"
wasm-bindgen = "0.2.54"
console_error_panic_hook = "0.1.6"

[dependencies.web-sys]
version = "0.3.4"

features = [
    'console',
    'Window',
    'Document',
    'DocumentFragment',
    'Node',
    'Element',
    'HtmlElement',
    'HtmlSlotElement',
    'HtmlTemplateElement',
    'CustomElementRegistry',
    'ShadowRoot',
    'ShadowRootInit',
    'ShadowRootMode',
]            

In the above, we specify the use of wasm-bindgen (0.2.52 or above is required), web-sys and js-sys. As described earlier, wasm-bindgen is used for facilitating high-level interactions between wasm modules and JavaScript. For example, if we need to use the browsers "Window" or "Document" objects or other functions in our rustwasm application, we can use wasm-bindgen to import and use them. It is a low-level library to ensure that everything we require from the browser (all Web APIs and JavaScript objects) can be imported to our rustwasm application.

In our Web Component, we also web-sys and js-sys, which are built on top of wasm-bindgen, to further ease our development process. In the above, we have specified the use the "console", "Window", "Document", "Node" and "Element" and other features from web-sys. When our application is compiled, all the wasm-bindgen bindings and imports will be automatically generated for us, saving us a lot of work.

With the features specified above, we can write a rustwasm application that uses the different browser objects to add a "Hello World" paragraph element as shown below.


use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {

    let window = web_sys::window().expect("no window exists");
    let document = window.document().expect("window should have a document");
    let body = document.body().expect("document should have a body");                
    let val = document.create_element("p")?;
    val.set_inner_html("Hello World from webassemblyMan!");                
    body.append_child(&val)?;                
    Ok(())
}       

The addition thing to note in "Cargo.toml" is that we have also enabled "HtmlElement" and "ShadowRoot" etc. which are objects we require for defining a Web Component.

4. Next, use a text editor and create a "index.html" file. Change the content of the file to the following:


<html>

<head>
<meta charset="utf-8">
<title>WebAssembly WebComponent</title>  
<script src="main.js" defer></script>  
</head>
<body>

<h1>WebComponent Template</h1>
<template id="webassembly-webcomponent">
    <style>
    p {
    color: white;
    background-color: #666;
    padding: 5px;

    }
    </style>
    <p><slot name="webcomponent-text">Default Text</slot></p>
</template>

<webassembly-webcomponent>
    <span slot="webcomponent-text">Hello World!</span>
</webassembly-webcomponent>

<webassembly-webcomponent>
    <ul slot="webcomponent-text">
    <li>rustwasm</li>
    <li>Emscripten</li>
    <li>Blazor</li>
    </ul>
</webassembly-webcomponent>
</body>
</html>
        

This is a HTML file that defines a template and CSS style for our Web Component.

By now, you would have noticed that we have defined a "webassembly-webcomponent" tag. To use this tag (Web Component), we can specify the following in our HTML file.


<webassembly-webcomponent>
    <span slot="webcomponent-text">Hello World!</span>
</webassembly-webcomponent>

5. Create a "main.js" to load our wasm (WebAssembly) module.


export default import('./pkg').catch(console.error);

6. Use a text editor, open "src/lib.rs" and change the contents of the file to the following:


use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[wasm_bindgen(prototype=web_sys::HtmlElement)]    
pub struct WebAssemblyWebComponent;

#[wasm_bindgen]
impl WebAssemblyWebComponent {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmType<WebAssemblyWebComponent> {

    let owned = instantiate! {
        super();
        WebAssemblyWebComponent
    };

    let this = owned.borrow();
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let template = document
        .get_element_by_id("webassembly-webcomponent").unwrap()
        .unchecked_into::<web_sys::HtmlTemplateElement>();

    let template_content = template.content();
    this.attach_shadow(
        &web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open)
    ).unwrap()
        .append_child(&template_content.clone_node_with_deep(true)
        .unwrap()).unwrap();
}
}
        
#[wasm_bindgen(start)]        
pub fn main() {

    console_error_panic_hook::set_once();
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();

    window.custom_elements().define(
        webassembly-webcomponent",
        &js_sys::Function::of::<WebAssemblyWebComponent>(),

    ).unwrap();

    let slotted_span = 
        document.query_selector("webassembly-webcomponent span")
        .unwrap().unwrap();        
    web_sys::console::log_1(&slotted_span.assigned_slot().unwrap());        
    web_sys::console::log_1(&slotted_span.slot().into());

} 
                

The first part of the rustwasm code above defines a "WebAssemblyWebComponent" struct using "web_sys::HtmlElement". The template we defined in our HTML page is read in (imported) with the Rust code below.


.
.
.
let template = document
.get_element_by_id("webassembly-webcomponent").unwrap()
.unchecked_into::();
.
.
.        
    

We also use web-sys to perform a DOM (Document Object Model) manipulation to create the element's internal shadow DOM structure.


.
.
.
this.attach_shadow(
&web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open)
).unwrap()
.append_child(&template_content.clone_node_with_deep(true)
.unwrap()).unwrap();
.
.
.        

The "main" function register our custom element. We have also used web_sys::console::log_1 to log the slot placeholder in our Web Component to the browser's console.


.
.
.                
#[wasm_bindgen(start)]        
pub fn main() {

console_error_panic_hook::set_once();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();

window.custom_elements().define(
"webassembly-webcomponent",
&js_sys::Function::of::<WebAssemblyWebComponent>(),

).unwrap();

let slotted_span = 
document.query_selector("webassembly-webcomponent span")
.unwrap().unwrap();        
web_sys::console::log_1(&slotted_span.assigned_slot().unwrap());        
web_sys::console::log_1(&slotted_span.slot().into());

} 
.
.
.                

7. Compile the rustwasm code with the following command. The wasm module is generated in the "pkg" folder.

wasm-pack build --target web

8. Run our development Web Server with the following command.

python3 -m http.server

9. You should see the following output on your browser:

[IMAGE]

Web Component Lifecycle callbacks

The following lifecycle callbacks of a Web Component are also supported "natively" in rustwasm.

  • connectedCallback - executed when the element is added to the DOM
  • disconnectedCallback - executed when the element is removed from the DOM
  • adoptedCallback - executed when the element moved to a different page
  • attributeChangedCallback - executed when one of the element's attributes is changed


// Specify observed attributes so that
// attributeChangedCallback will work
#[wasm_bindgen(getter = observedAttributes)]
pub fn observed_attributes() -> js_sys::Array {
.
.
.
}

#[wasm_bindgen(js_name="connectedCallback")]
pub fn connected_callback(&self) {
    web_sys::console::log_1(&"connectedCallback.".into());
}

#[wasm_bindgen(js_name="disconnectedCallback")]
pub fn disconnected_callback(&self) {
    web_sys::console::log_1(&"disconnectedCallback.".into());
}

#[wasm_bindgen(js_name="adoptedCallback")]
pub fn adopted_callback(&self) {
    web_sys::console::log_1(&"adoptedCallback.".into());
}

#[wasm_bindgen(js_name="attributeChangedCallback")]
pub fn attribute_changed_callback(&self, _name: &str, 
            _old_value: Option<String>, _new_value: &str) {
    web_sys::console::log_1(&"attributeChangedCallback.".into());
}            
            

Source Code

rustwasmWebAssemblyWebComponent.zip

Summary

Our tutorial and sample have illustrated how you can create a W3C Web Component (Custom Element) "natively using rustwasm WebAssembly, with the help of wasm-bindgen, web-sys and js-sys. If you recall, all our codes involve almost little or almost no JavaScript. Please forgive me for saying this, it appears that rustwasm is attempting to replace JavaScript with Rust in the browser "TOTALLY". Yes, you heard it right, "TOTALLY".

The other thing interesting to note is rustwasm is doing all this without specifically requiring a runtime or framework. This way, you can replace parts of your front-end web application surgically. Alternatively, frameworks that is built on top of rustwasm, has started appearing, to help you replace your front-end application in a "big bang" manner. rustwasm flexibility, together with its optimized output code size will appeal to many developers.

Oh, did I forget to mention the WASI (WebAssembly outside the browser) and the Gloo project of rustwasm?