#web #web-components #event-handling #events #webcomponent

rs_web_component

Helps to create a custom element for a web project

8 releases

0.1.7 Feb 4, 2024
0.1.6 Feb 4, 2024
0.1.5 Jan 9, 2024
0.1.2 Dec 27, 2023

#218 in WebAssembly


Used in xml_viewer

MIT license

22KB
176 lines

A sample project that shows how a custom component can be created in Rust.

v0.1.7

  • Description in the Cargo.toml file corrected
  • Link to documentation corrected

v0.1.6

  • The JS snippet was removed. No more JS snippets, just Rust 😀.
  • Fixed documentation

v0.1.5

v0.1.4:

v0.1.3:

  • The function add_template was added
  • An example with a template was added

Documentation v0.1.7

Examples:

Basic example:

use rs_web_component::{define_element, Component};
use wasm_bindgen::prelude::*;
use web_sys::{HtmlElement, ShadowRoot, ShadowRootInit, ShadowRootMode};

pub enum ThisVal {
    Value(HtmlElement),
    None,
}

pub enum RootVal {
    Value(ShadowRoot),
    None,
}

struct MyComponent {
    root: RootVal,
    this: ThisVal,
}

impl Component for MyComponent {
    fn init(&mut self, this: HtmlElement) {
        self.this = ThisVal::Value(this);
    }

    fn observed_attributes(&self) -> Vec<String> {
        return vec!["test".to_string()];
    }

    fn attribute_changed_callback(&self, _name: String, _old_value: String, _new_value: String) {
        if _old_value != _new_value {
            self.get_root().set_inner_html(self.render().as_str())
        }
    }

    fn connected_callback(&mut self) {
        self.root = RootVal::Value(
            self.get_this()
                .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
                .unwrap(),
        );

        self.get_root().set_inner_html(self.render().as_str())
    }

    fn disconnected_callback(&self) {}
}

impl MyComponent {
    fn render(&self) -> String {
        "<div><span>Hello from Rust</span></div>".to_string()
    }

    fn get_root(&self) -> &ShadowRoot {
        return match &self.root {
            RootVal::Value(root) => &root,
            RootVal::None => panic!("not a root!"),
        };
    }

    fn get_this(&self) -> &HtmlElement {
        match &self.this {
            ThisVal::Value(val) => val,
            ThisVal::None => panic!("not an HtmlElement"),
        }
    }
}

#[wasm_bindgen(start)]
fn run() {
    define_element("test-component".to_string(), || -> Box<dyn Component> {
        Box::new(MyComponent {
            root: RootVal::None,
            this: ThisVal::None,
        })
    });
}

Example with the event handler:

use rs_web_component::{define_element, Component};
use wasm_bindgen::prelude::*;
use web_sys::{
    CustomEvent, CustomEventInit, Event, HtmlElement, ShadowRoot, ShadowRootInit, ShadowRootMode,
};

const BUTTON_EVENT_NAME: &str = "buttonClicked";

pub enum ThisVal {
    Value(HtmlElement),
    None,
}

pub enum RootVal {
    Value(ShadowRoot),
    None,
}

pub enum CallbackVal {
    Value(Closure<dyn FnMut(Event) + 'static>),
    None,
}

struct MyComponent {
    root: RootVal,
    this: ThisVal,
    callback: CallbackVal,
}

impl Component for MyComponent {
    fn init(&mut self, this: HtmlElement) {
        self.this = ThisVal::Value(this);
    }

    fn observed_attributes(&self) -> Vec<String> {
        return vec!["test".to_string()];
    }

    fn attribute_changed_callback(&self, _name: String, _old_value: String, _new_value: String) {}

    fn connected_callback(&mut self) {
        self.root = RootVal::Value(
            self.get_this()
                .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
                .unwrap(),
        );

        self.get_root().set_inner_html(self.render().as_str());
        self.attach_event_handler();
    }

    fn disconnected_callback(&self) {
        self.detach_event_handler();
    }
}

impl MyComponent {
    fn render(&self) -> String {
        "<div><button>Click me</button></div>".to_string()
    }

    fn attach_event_handler(&mut self) {
        let btn = self.get_root().query_selector("button").unwrap().unwrap();
        let closure = Closure::<dyn FnMut(Event) + 'static>::new(move |e: Event| {
            let evt = CustomEvent::new_with_event_init_dict(
                BUTTON_EVENT_NAME,
                CustomEventInit::new().composed(true).bubbles(true),
            )
            .unwrap();
            let _ = btn.dispatch_event(&evt);
        });
        self.callback = CallbackVal::Value(closure);
        let btn = self.get_root().query_selector("button").unwrap().unwrap();
        let _ = btn.add_event_listener_with_callback(
            "click",
            self.get_callback().as_ref().unchecked_ref(),
        );
    }

    fn detach_event_handler(&self) {
        let btn = self.get_this().query_selector("button").unwrap().unwrap();
        let _ = btn.remove_event_listener_with_callback(
            "click",
            self.get_callback().as_ref().unchecked_ref(),
        );
    }

    fn get_callback(&self) -> &Closure<dyn FnMut(Event) + 'static> {
        return match &self.callback {
            CallbackVal::Value(callback) => callback,
            &CallbackVal::None => panic!("not a callback!"),
        };
    }

    fn get_root(&self) -> &ShadowRoot {
        return match &self.root {
            RootVal::Value(root) => &root,
            RootVal::None => panic!("not a root!"),
        };
    }

    fn get_this(&self) -> &HtmlElement {
        match &self.this {
            ThisVal::Value(val) => val,
            ThisVal::None => panic!("not an HtmlElement"),
        }
    }
}

#[wasm_bindgen(start)]
fn run() {
    define_element("test-component".to_string(), || -> Box<dyn Component> {
        Box::new(MyComponent {
            root: RootVal::None,
            this: ThisVal::None,
            callback: CallbackVal::None,
        })
    });
}

Dependencies

~7.5–10MB
~182K SLoC