This crate provides a bridge to expose a Dioxus component as a web component.
This crate supports web component attributes and custom events. You can also add CSS style to your web component.
Take a look at the examples to see the usage in a full project: https://github.com/ilaborie/dioxus-web-component/tree/main/examples
If you are new to WebAssembly with Rust, take a look at the Rust WebAssembly book first.
See [web_component] macro documentation for more details.
Ideally, you only need to replace the Dioxus #[component] by #[web_component].
Then you should register the web component with wasm-bindgen.
To finish, you can create the npm package with wasm-pack.
use dioxus::prelude::*;
use dioxus_web_component::web_component;
use wasm_bindgen::prelude::*;
#[web_component]
fn MyWebComponent(
attribute: String,
on_event: EventHandler<i64>,
) -> Element {
todo!()
}
// Function to call from the JS side
#[wasm_bindgen]
pub fn register() {
// Register the web component (aka custom element)
register_my_web_component();
}Then call the function from the JS side.
The #[web_component] annotation can be configured with:
tagto set the HTML custom element tag name. By default, it's the kebab case version of the function name.styleto provide the [InjectedStyle] to your component.
The parameters of the component could be:
- an attribute if you want to pass the parameter as an HTML attribute,
- a property if you only want to read/write the parameter as a property of the Javascript
HTMLElement, - or an event if the parameter is a Dioxus
EventHandler.
💡TIP: You can be an attribute AND a property if you use the two annotations.
Attributes are pure HTML attributes, should be deserialize from string.
Attributes can be customized with the #[attribute] annotation with:
nameto set the HTML attribute name. By default, it's the kebab-case of the parameter name.optionto mark the attribute optional.trueby default if the type isOption<...>.initialto set the default value when the HTML attribute is missing By default use thestd::default::Defaultimplementation of the type.parseto provide the conversion between the HTML attribute value (a string) to the type value. By default use thestd::str::FromStrimplementation, and fall to the default value if it fails.
Properties are custom properties accessible from Javascript.
To declare a property, you need to use the #[property] annotation.
We use wasm-bindgen to convert the Rust side value to a Javascript value.
You can customize the property with these attributes:
nameto set the Javascript name of the property. By default, it's the camelCase of the parameter name.readonlyto only generate the custom getterinitialto set the default value when the HTML attribute is missing By default use thestd::defaultDefaultimplementation of the type.try_from_jsto provide the conversion from aJsValueto the parameter type. By default use thestd::convert::TryIntoimplementation. The error case is ignored (does not set the value)try_into_jsto provide the conversion from the parameter type to aJsValue. By default use thestd::convert::TryIntoimplementation. Returnundefinedin case of error
Events are parameters with the Dioxus EventHandler<...> type.
You can customize the event with these attributes:
nameto set the HTML event name. By default use the parameter name without theonprefix (if any)no_bubbleto forbid the custom event from bubblingno_cancelto remove the ability to cancel the custom event
Currently, the idea is to avoid breaking changes when you use the macros, but you should expect to have some in the API.
The usage without macro is discouraged
You can provide your manual implementation of [DioxusWebComponent] and call
[register_dioxus_web_component] to register your web component.
The key point is to use a Shared element in the dioxus context.
For example, the greeting example could be written with
use dioxus::prelude::*;
use dioxus_web_component::{
register_dioxus_web_component, DioxusWebComponent, InjectedStyle, Message, Property, Shared,
};
use wasm_bindgen::prelude::*;
/// Install (register) the web component
#[wasm_bindgen(start)]
pub fn register() {
register_greetings();
}
#[component]
fn Greetings(name: String) -> Element {
rsx! { p { "Hello {name}!" } }
}
fn register_greetings() {
let properties = vec![Property::new("name", false)];
let style = InjectedStyle::css(include_str!("style.css"));
register_dioxus_web_component(
"plop-greeting",
vec!["name".to_string()],
properties,
style,
greetings_builder,
);
}
#[derive(Clone, Copy)]
struct GreetingsWebComponent {
name: Signal<String>,
}
impl DioxusWebComponent for GreetingsWebComponent {
fn set_attribute(&mut self, attribute: &str, value: Option<String>) {
match attribute {
"name" => {
let new_value = value.and_then(|attr| attr.parse().ok()).unwrap_or_default();
self.name.set(new_value);
}
_ => {
// nop
}
}
}
fn set_property(&mut self, property: &str, value: JsValue) {
match property {
// we allow to set the name as a property
"name" => {
if let Ok(new_value) = Ok(value).and_then(|value| value.try_into()) {
self.name.set(new_value);
}
}
_ => {
// nop
}
}
}
fn get_property(&mut self, property: &str) -> JsValue {
match property {
// we allow to get the name as a property
"name" => Ok(self.name.read().clone())
.and_then(|value| value.try_into())
.unwrap_or(::wasm_bindgen::JsValue::NULL),
_ => JsValue::undefined(),
}
}
}
fn greetings_builder() -> Element {
let mut wc = use_context::<Shared>();
let name = use_signal(String::new);
let mut greetings = GreetingsWebComponent { name };
let coroutine = use_coroutine::<Message, _, _>(move |mut rx| async move {
use dioxus_web_component::StreamExt;
while let Some(msg) = rx.next().await {
greetings.handle_message(msg);
}
});
use_effect(move || {
wc.set_tx(coroutine.tx());
});
rsx! {
Greetings {
name
}
}
}The counter example looks like this:
use dioxus::prelude::*;
use dioxus_web_component::{
custom_event_handler, register_dioxus_web_component, CustomEventOptions, DioxusWebComponent,
};
use dioxus_web_component::{InjectedStyle, Message, Property, Shared};
use wasm_bindgen::prelude::*;
/// Install (register) the web component
///#[wasm_bindgen(start)]
pub fn register(){
// The register counter is generated by the `#[web_component(...)]` macro
register_counter();
}
/// The Dioxus component
#[component]
fn Counter(label: String, on_count: EventHandler<i32>) -> Element {
let mut counter = use_signal(|| 0);
rsx! {
span { "{label}" }
button {
onclick: move |_| {
counter += 1;
on_count(counter());
},
"+"
}
output { "{counter}" }
}
}
fn register_counter() {
let properties = vec![Property::new("label", false)];
let style = InjectedStyle::stylesheet("./style.css");
register_dioxus_web_component("plop-counter", vec![], properties, style, counter_builder);
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
struct CounterWebComponent {
label: Signal<String>,
on_count: EventHandler<i32>,
}
impl DioxusWebComponent for CounterWebComponent {
#[allow(clippy::single_match_else)]
fn set_property(&mut self, property: &str, value: JsValue) {
match property {
"label" => {
let new_value = String::try_from(value).unwrap_throw();
self.label.set(new_value);
}
_ => {
// nop
}
}
}
#[allow(clippy::single_match_else)]
fn get_property(&mut self, property: &str) -> JsValue {
match property {
"label" => {
let value = self.label.read().clone();
value.into()
}
_ => JsValue::undefined(),
}
}
}
fn counter_builder() -> Element {
let mut wc = use_context::<Shared>();
let label = use_signal(String::new);
let on_count = custom_event_handler(wc.event_target().clone(), "count", CustomEventOptions::default());
let mut counter = CounterWebComponent { label, on_count };
let coroutine = use_coroutine::<Message, _, _>(move |mut rx| async move {
use dioxus_web_component::StreamExt;
while let Some(msg) = rx.next().await {
counter.handle_message(msg);
}
});
use_effect(move || {
wc.set_tx(coroutine.tx());
});
rsx! {
Counter {
label,
on_count
}
}
}- only extends
HTMLElement - only work as a replacement of Dioxus
#[component]annotation (does not work with handmadeProps) - cannot add a method callable from Javascript in the web component.
- property getters return a JS promise
Contributions are welcome ❤️.