# Dioxus Web Component
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.
## Usage with macro
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].
```rust
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.
### Customization of the web component
The `#[web_component]` annotation can be configured with:
* `tag` to set the HTML custom element tag name.
By default, it's the kebab case version of the function name.
* `style` to 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
Attributes are pure HTML attributes, should be deserialize from string.
Attributes can be customized with the `#[attribute]` annotation with:
* `name` to set the HTML attribute name.
By default, it's the kebab-case of the parameter name.
* `option` to mark the attribute optional.
`true` by default if the type is `Option<...>`.
* `initial` to set the default value when the HTML attribute is missing
By default use the `std::default::Default` implementation of the type.
* `parse` to provide the conversion between the HTML attribute value (a string) to the type value.
By default use the `std::str::FromStr` implementation, and fall to the default value if it fails.
#### Property
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:
* `name` to set the Javascript name of the property.
By default, it's the camelCase of the parameter name.
* `readonly` to only generate the custom getter
* `initial` to set the default value when the HTML attribute is missing
By default use the `std::defaultDefault` implementation of the type.
* `try_from_js` to provide the conversion from a `JsValue` to the parameter type.
By default use the `std::convert::TryInto` implementation.
The error case is ignored (does not set the value)
* `try_into_js` to provide the conversion from the parameter type to a `JsValue`.
By default use the `std::convert::TryInto` implementation.
Return `undefined` in case of error
⚠️ WARN: reading a property value return a JS Promise.
#### Events
Events are parameters with the Dioxus `EventHandler<...>` type.
You can customize the event with these attributes:
* `name` to set the HTML event name.
By default use the parameter name without the `on` prefix (if any)
* `no_bubble` to forbid the custom event from bubbling
* `no_cancel` to remove the ability to cancel the custom event
## Usage without macro
Currently, the idea is to avoid breaking changes when you use the macros,
but you should expect to have some in the API.
<details>
<summary>The usage without macro is discouraged</summary>
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
```rust, ignore
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:
```rust
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
}
}
}
```
</details>
## Limitations
* only extends `HTMLElement`
* only work as a replacement of Dioxus `#[component]` annotation (does not work with handmade `Props`)
* cannot add a method callable from Javascript in the web component.
* property getters return a JS promise
## Contributions
Contributions are welcome ❤️.
[Dioxus]: https://dioxuslabs.com/
[web component]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
[wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen
[npm]: https://www.npmjs.com/
[wasm-pack]: https://github.com/rustwasm/wasm-pack
[Rust WebAssembly book]: https://rustwasm.github.io/docs/book/
[dioxus-web-component-macro]: https://github.com/ilaborie/dioxus-web-component/blob/main/dioxus-web-component-macro/README.md