#dioxus #web-component

dioxus-web-component

将Dioxus组件暴露为Web组件

11个不稳定版本 (3个重大变更)

0.3.2 2024年6月2日
0.3.1 2024年6月1日
0.3.0 2024年5月31日
0.2.2 2024年5月25日
0.0.2 2024年4月28日

#119WebAssembly

Download history 335/week @ 2024-04-27 14/week @ 2024-05-04 423/week @ 2024-05-11 457/week @ 2024-05-18 301/week @ 2024-05-25 393/week @ 2024-06-01 25/week @ 2024-06-08 4/week @ 2024-06-15 52/week @ 2024-06-29 150/week @ 2024-07-27

每月196次下载

MIT/Apache

29KB
377

Dioxus Web Component

此crate提供了一种将Dioxus组件暴露为web组件的桥梁。

此crate支持web组件属性和自定义事件。您还可以为您的web组件添加CSS样式。

查看示例,了解如何在完整项目中使用: https://github.com/ilaborie/dioxus-web-component/tree/main/examples

如果您是Rust和WebAssembly的新手,请先查看Rust WebAssembly书籍

使用宏的使用

理想情况下,您只需要将Dioxus的#[component]替换为#[web_component]。然后您应该使用wasm-bindgen注册web组件。最后,您可以使用npmwasm-pack创建npm包。

use dioxus::prelude::*;
use dioxus_web_component::web_component;

#[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();
}

然后在JS侧调用该函数。

Web组件的定制

#[web_component]注解可以通过以下方式配置

  • tag来设置HTML自定义元素标签名。默认情况下,它是函数名的短横线版本。
  • style提供InjectedStyle到您的组件。

组件的参数可以是

  • 一个属性,如果您想将参数作为HTML属性传递,
  • 一个属性,如果您只想将参数作为JavaScript HTMLElement的属性读取/写入,
  • 或一个事件,如果参数是Dioxus EventHandler

💡提示:如果您使用这两个注解,则可以将属性和特性同时使用。

属性

您可以使用 #[attribute] 注解来定制属性,并使用

  • name 来设置 HTML 属性名称。默认情况下,它是参数名称的短横线命名法。
  • option 用于标记属性为可选。如果类型是 Option<...>
  • initial 用于在 HTML 属性缺失时设置默认值。默认使用该类型的 std::default::Default 实现来设置默认值。
  • parse 用于提供 HTML 属性值(字符串)与类型值之间的转换。默认使用 std::str::FromStr 实现进行转换,如果转换失败则使用默认值。

特性

要声明特性,您需要使用 #[property] 注解。

我们使用 wasm-bindgen 将 Rust 端的值转换为 JavaScript 值。

⚠️ 重要:getter 返回一个 JavaScript Promise

您可以使用以下属性来定制特性

  • name 用于设置特性的 JavaScript 名称。默认情况下,它是参数名称的驼峰命名法。
  • readonly 用于仅生成自定义 getter
  • initial 用于在 HTML 属性缺失时设置默认值。默认使用该类型的 std::defaultDefault 实现来设置默认值。
  • try_from_js 用于从 JsValue 转换到参数类型。默认使用 std::convert::TryInto 实现进行转换。错误情况被忽略(不会设置值)
  • try_into_js 用于从参数类型转换到 JsValue。默认使用 std::convert::TryInto 实现进行转换。在错误情况下返回 undefined

事件

事件是具有 Dioxus EventHandler<...> 类型的参数。您可以使用以下属性来定制事件

  • name 用于设置 HTML 事件名称。默认情况下,使用不带 on 前缀的参数名称(如果有的话)
  • no_bubble 用于禁止自定义事件冒泡
  • no_cancel 用于移除取消自定义事件的能力

此示例使用所有注解

use dioxus::prelude::*;
use dioxus_web_component::{web_component, InjectedStyle};

#[web_component(tag = "my-component", style = InjectedStyle::css(include_str!("./style.css")))]
fn MyWebComponent(
    #[attribute(name = "attr1", option = false, initial = String::new(), parse = |value| Some(value.to_string()))]
    attr1: String,
    #[attribute(name = "attr-option", option = true, initial = None, parse = |value| Some(value.to_string()))]
    attr_option: Option<String>,
    // Readonly property
    #[property(readonly)]
    prop: Option<String>,
    // Property with custom conversion
    #[property(
        initial = MyProp(true),
        try_into_js = |prop| {
            let js_value = if prop.0 {
                JsValue::TRUE
            } else {
                JsValue::FALSE
            };
            Ok::<_, Infallible>(js_value)
        },
        try_from_js= |value| Ok::<_, Infallible>(MyProp(value.is_truthy())),
    )]
    prop2: MyProp,
    #[event(name = "event", no_bubble = false, no_cancel = false)] event: EventHandler<i64>,
) -> Element {
    todo!()
}

#[derive(Clone, PartialEq)]
struct MyProp(bool);

有关更多详细信息,请参阅 dioxus-web-component-macro 文档。

无宏的使用方法

目前,当您使用宏时,我们旨在避免破坏性更改,但您应该预计在 API 中会有一些更改。

不建议使用无宏的使用方法

您可以为 DioxusWebComponent 提供您的手动实现,并调用 register_dioxus_web_component 来注册您的网页组件。

关键点是使用 dioxus 上下文中的 Shared 元素。

例如,问候示例可以写成

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
///
/// # Errors
///
/// Registering the web-component may fail
#[wasm_bindgen(start)]
pub fn register() -> Result<(), JsValue> {
    register_greetings();
    Ok(())
}

#[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 corountine = 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(corountine.tx());
    });

    rsx! {
        Greetings {
            name
        }
    }
}

计数器示例看起来像这样

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
///
/// # Errors
///
/// Registering the web-component may fail
#[wasm_bindgen(start)]
pub fn register() -> Result<(), JsValue> {
    // The register counter is generated by the `#[web_component(...)]` macro
    register_counter();
    Ok(())
}

/// 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::(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(), "count", CustomEventOptions::default());

    let mut counter = CounterWebComponent { label, on_count };
    let corountine = 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(corountine.tx());
    });

    rsx! {
        Counter {
            label,
            on_count
        }
    }
}

限制

  • 仅扩展 HTMLElement
  • 仅作为 Dioxus #[component] 注释的替代品(不与手工 Props 一起使用)
  • 无法在网页组件中添加可从 JavaScript 调用的方法。(解决方案:使用属性)

贡献

欢迎贡献 ❤️。

依赖

~17–26MB
~400K SLoC