dioxus_web_component/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(clippy::multiple_crate_versions)]
3
4use std::sync::Arc;
5use std::sync::RwLock;
6
7use dioxus::dioxus_core::Element;
8use dioxus::hooks::UnboundedSender;
9use dioxus::logger::tracing::debug;
10use futures::channel::oneshot;
11use wasm_bindgen::prelude::*;
12use web_sys::HtmlElement;
13
14use crate::rust_component::RustComponent;
15
16pub use dioxus_web_component_macro::web_component;
17
18mod event;
19pub use self::event::*;
20
21mod style;
22pub use self::style::*;
23
24mod rust_component;
25
26/// Re-export, use this trait in the coroutine
27pub use futures::StreamExt;
28
29/// Message from web component to dioxus
30#[derive(Debug)]
31#[non_exhaustive]
32pub enum Message {
33    /// Set attribute
34    SetAttribute {
35        /// Attribute name
36        name: String,
37        /// Attribute value
38        value: Option<String>,
39    },
40    /// Get property
41    Get {
42        /// Property name
43        name: String,
44        /// reply channel
45        tx: oneshot::Sender<SharedJsValue>,
46    },
47    /// Set property
48    Set {
49        /// Property name
50        name: String,
51        /// Property value
52        value: SharedJsValue,
53    },
54}
55
56#[derive(Clone)]
57struct SharedEventTarget(web_sys::HtmlElement);
58
59#[allow(unsafe_code)]
60// SAFETY:
61// In a Web WASM context, without thread.
62// This only be used to display an event, no update are made here
63unsafe impl Send for SharedEventTarget {}
64
65#[allow(unsafe_code)]
66// SAFETY:
67// In a Web WASM context, without thread.
68// This only be used to display an event, no update are made here
69unsafe impl Sync for SharedEventTarget {}
70
71#[doc(hidden)]
72#[derive(Debug, Clone)]
73pub struct SharedJsValue(JsValue);
74
75#[allow(unsafe_code)]
76// SAFETY:
77// In a Web WASM context, without thread.
78// This only be used to display an event, no update are made here
79unsafe impl Send for SharedJsValue {}
80
81#[allow(unsafe_code)]
82// SAFETY:
83// In a Web WASM context, without thread.
84// This only be used to display an event, no update are made here
85unsafe impl Sync for SharedJsValue {}
86
87/// A context provided by the web component
88#[derive(Clone)]
89pub struct Shared {
90    attributes: Vec<String>,
91    event_target: SharedEventTarget,
92    tx: Arc<RwLock<Option<UnboundedSender<Message>>>>,
93}
94
95impl Shared {
96    /// The web component event target use to dispatch custom event
97    #[must_use]
98    pub fn event_target(&self) -> &HtmlElement {
99        &self.event_target.0
100    }
101
102    /// Set the receiver
103    pub fn set_tx(&mut self, tx: UnboundedSender<Message>) {
104        // initial state
105        let trg = self.event_target();
106        for attr in &self.attributes {
107            let Some(value) = trg.get_attribute(attr) else {
108                continue;
109            };
110            let _ = tx.unbounded_send(Message::SetAttribute {
111                name: attr.to_string(),
112                value: Some(value),
113            });
114        }
115
116        // Keep sender (skip if poisoned)
117        if let Ok(mut cell) = self.tx.write() {
118            *cell = Some(tx);
119        }
120    }
121}
122
123/// Dioxus web component
124pub trait DioxusWebComponent {
125    /// Set an HTML attribute
126    fn set_attribute(&mut self, attribute: &str, value: Option<String>) {
127        let _ = value;
128        let _ = attribute;
129    }
130
131    /// Set a property
132    fn set_property(&mut self, property: &str, value: JsValue) {
133        let _ = value;
134        let _ = property;
135    }
136
137    /// Get a property
138    fn get_property(&mut self, property: &str) -> JsValue {
139        let _ = property;
140        JsValue::undefined()
141    }
142
143    /// Handle a message
144    fn handle_message(&mut self, message: Message) {
145        debug!(?message, "handle message");
146        match message {
147            Message::SetAttribute { name, value } => self.set_attribute(&name, value),
148            Message::Get { name, tx } => {
149                let value = self.get_property(&name);
150                let _ = tx.send(SharedJsValue(value));
151            }
152            Message::Set { name, value } => self.set_property(&name, value.0),
153        }
154    }
155}
156
157/// Property
158#[wasm_bindgen(skip_typescript)]
159#[derive(Debug, Clone)]
160pub struct Property {
161    /// Name
162    name: String,
163    /// Readonly
164    readonly: bool,
165}
166
167impl Property {
168    /// Create a property
169    pub fn new(name: impl Into<String>, readonly: bool) -> Self {
170        let name = name.into();
171        Self { name, readonly }
172    }
173}
174
175#[wasm_bindgen]
176impl Property {
177    /// Get name
178    #[wasm_bindgen(getter)]
179    #[must_use]
180    pub fn name(&self) -> String {
181        self.name.clone()
182    }
183
184    /// Is property readonly
185    #[wasm_bindgen(getter)]
186    #[must_use]
187    pub fn readonly(&self) -> bool {
188        self.readonly
189    }
190}
191
192/// Register a Dioxus web component
193pub fn register_dioxus_web_component(
194    custom_tag: &str,
195    attributes: Vec<String>,
196    properties: Vec<Property>,
197    style: InjectedStyle,
198    dx_el_builder: fn() -> Element,
199) {
200    let rust_component = RustComponent {
201        attributes,
202        properties,
203        style,
204        dx_el_builder,
205    };
206    register_web_component(custom_tag, rust_component);
207}
208
209#[wasm_bindgen(module = "/src/shim.js")]
210extern "C" {
211    #[allow(unsafe_code)]
212    fn register_web_component(custom_tag: &str, rust_component: RustComponent);
213}