2022-10-02 18:16:10 -04:00
|
|
|
use js_sys::Function;
|
2022-10-05 20:31:29 -04:00
|
|
|
use wasm_bindgen::{convert::IntoWasmAbi, prelude::Closure, JsValue};
|
2022-10-02 20:27:01 -04:00
|
|
|
use web_sys::{window, Element, HtmlElement};
|
2022-10-02 18:16:10 -04:00
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
/// This attribute proc-macro will generate the following trait implementations
|
|
|
|
/// * [WebComponentDef](trait@WebComponentDef)
|
|
|
|
/// * [WebComponent](trait@WebComponent)
|
|
|
|
///
|
|
|
|
/// It will also generate a wasm_bindgen compatible impl block for your struct.
|
|
|
|
///
|
|
|
|
/// It expects you to implement the [WebComponentBinding](trait@WebComponentBinding)
|
|
|
|
/// trait in order to implement the callbacks.
|
|
|
|
///
|
|
|
|
/// It supports three attribute `name = value` parameters.
|
|
|
|
/// * `class_name = "ClassName"` - Required. The class name to use for the javascript shim.
|
|
|
|
/// * `element_name = "class-name"` - Optional. A valid custom element name to use for the element.
|
|
|
|
/// * `observed_attrs = "['attr1', attr2']"` - Optional. A javascript array with a list of observed attributes for this compoment.
|
|
|
|
///
|
|
|
|
/// Reference [MDN Web Components Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
|
|
|
|
pub use wasm_web_component_macros::web_component;
|
2022-10-05 18:36:26 -04:00
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
/// Helper trait for Rust Web Components. This is autogenerated
|
|
|
|
/// by the [`#[web_component]`](wasm_web_component_macros::web_component) attribute.
|
2022-10-05 20:31:29 -04:00
|
|
|
pub trait WebComponentDef: IntoWasmAbi + Default {
|
|
|
|
fn new() -> Self {
|
|
|
|
Self::default()
|
|
|
|
}
|
2022-10-02 18:16:10 -04:00
|
|
|
|
2022-10-05 20:31:29 -04:00
|
|
|
fn create() -> Element {
|
|
|
|
window()
|
|
|
|
.unwrap()
|
|
|
|
.document()
|
|
|
|
.unwrap()
|
|
|
|
.create_element(Self::element_name())
|
|
|
|
.unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn element_name() -> &'static str;
|
|
|
|
fn class_name() -> &'static str;
|
|
|
|
}
|
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
/// Trait defining the lifecycle callbacks for a Custom Element.
|
|
|
|
/// Each method is optional. You only need to implement the ones
|
|
|
|
/// you want to specify behavior for.
|
2022-10-05 20:31:29 -04:00
|
|
|
pub trait WebComponentBinding: WebComponentDef {
|
|
|
|
fn connected(&self, _element: &HtmlElement) {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
|
|
|
|
fn disconnected(&self, _element: &HtmlElement) {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
|
|
|
|
fn adopted(&self, _element: &HtmlElement) {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
|
|
|
|
fn attribute_changed(
|
|
|
|
&self,
|
|
|
|
_element: &HtmlElement,
|
|
|
|
_name: JsValue,
|
|
|
|
_old_value: JsValue,
|
|
|
|
_new_value: JsValue,
|
|
|
|
) {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
}
|
2022-10-02 18:21:24 -04:00
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
/// Marker trait used in the generated shims to assert that their are Rust implemtntations
|
|
|
|
/// of the callback functions for the component.
|
2022-10-05 20:31:29 -04:00
|
|
|
pub trait WebComponent: WebComponentBinding {}
|
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
/// A handle for your WebComponent Definition. It is important that this
|
|
|
|
/// handle is live for as long as your Web-Component might be used.
|
2022-10-05 20:31:29 -04:00
|
|
|
pub struct WebComponentHandle<T> {
|
2022-10-06 23:07:18 -04:00
|
|
|
/// The handle for the closure that is used to construct your Rust instance
|
|
|
|
/// in the Javascript shim constructor. If this is dropped then your web component
|
|
|
|
/// will not be able to be constructed properly.
|
2022-10-02 18:21:24 -04:00
|
|
|
pub impl_handle: Closure<dyn FnMut() -> T>,
|
2022-10-06 23:07:18 -04:00
|
|
|
/// A javascript function that can construct your element.
|
2022-10-02 18:21:24 -04:00
|
|
|
pub element_constructor: Function,
|
2022-10-02 18:16:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2022-10-05 20:31:29 -04:00
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
|
|
use wasm_bindgen::{JsCast, JsValue};
|
2022-10-02 18:16:10 -04:00
|
|
|
use wasm_bindgen_test::wasm_bindgen_test;
|
2022-10-02 20:27:01 -04:00
|
|
|
use web_sys::Text;
|
2022-10-05 20:31:29 -04:00
|
|
|
use web_sys::{window, HtmlElement};
|
2022-10-02 18:16:10 -04:00
|
|
|
|
2022-10-06 23:07:18 -04:00
|
|
|
use wasm_web_component_macros::web_component;
|
2022-10-02 18:16:10 -04:00
|
|
|
|
2022-10-05 20:31:29 -04:00
|
|
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
2022-10-02 20:27:01 -04:00
|
|
|
|
2022-10-02 18:16:10 -04:00
|
|
|
#[wasm_bindgen]
|
|
|
|
extern "C" {
|
|
|
|
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
|
|
|
pub fn log(message: String);
|
|
|
|
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
|
|
|
pub fn log_with_val(message: String, val: &JsValue);
|
|
|
|
}
|
|
|
|
|
2022-10-02 20:27:01 -04:00
|
|
|
// NOTE(jwall): We can only construct the web component once and since the lifetime of the component internals is tied
|
|
|
|
// to the handle we run this all in one single function.
|
2022-10-02 18:16:10 -04:00
|
|
|
#[wasm_bindgen_test]
|
2022-10-02 20:27:01 -04:00
|
|
|
fn test_component() {
|
2022-10-06 22:13:18 -04:00
|
|
|
#[web_component(
|
|
|
|
class_name = "MyElement",
|
|
|
|
element_name = "my-element",
|
|
|
|
observed_attrs = "['class']"
|
|
|
|
)]
|
|
|
|
pub struct MyElementImpl {}
|
|
|
|
|
|
|
|
impl WebComponentBinding for MyElementImpl {
|
|
|
|
fn connected(&self, element: &HtmlElement) {
|
|
|
|
log("Firing connected call back".to_owned());
|
|
|
|
let node = Text::new().unwrap();
|
|
|
|
node.set_text_content(Some("Added a text node on connect".into()));
|
|
|
|
element.append_child(&node).unwrap();
|
|
|
|
log(format!(
|
|
|
|
"element contents: {}",
|
|
|
|
&element.text_content().unwrap()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn disconnected(&self, element: &HtmlElement) {
|
|
|
|
log("Firing discconnected call back".to_owned());
|
|
|
|
let node = element.first_child().unwrap();
|
|
|
|
element.remove_child(&node).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn adopted(&self, element: &HtmlElement) {
|
|
|
|
log("Firing adopted call back".to_owned());
|
|
|
|
let node = Text::new().unwrap();
|
|
|
|
node.set_text_content(Some("Added a text node on adopt".into()));
|
|
|
|
element.append_child(&node).unwrap();
|
|
|
|
log_with_val("element: ".to_owned(), element);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn attribute_changed(
|
|
|
|
&self,
|
|
|
|
element: &HtmlElement,
|
|
|
|
name: JsValue,
|
|
|
|
old_value: JsValue,
|
|
|
|
new_value: JsValue,
|
|
|
|
) {
|
|
|
|
log("Firing attribute changed callback".to_owned());
|
|
|
|
let node = element.first_child().unwrap();
|
|
|
|
node.set_text_content(Some(&format!(
|
|
|
|
"Setting {} from {} to {}",
|
|
|
|
name.as_string().unwrap_or("None".to_owned()),
|
|
|
|
old_value.as_string().unwrap_or("None".to_owned()),
|
|
|
|
new_value.as_string().unwrap_or("None".to_owned()),
|
|
|
|
)));
|
|
|
|
element.append_child(&node).unwrap();
|
|
|
|
log_with_val("element: ".to_owned(), element);
|
|
|
|
}
|
|
|
|
}
|
2022-10-02 20:27:01 -04:00
|
|
|
let obj = MyElementImpl::define().expect("Failed to define web component");
|
2022-10-02 18:21:24 -04:00
|
|
|
let fun = obj.element_constructor.dyn_ref::<Function>().unwrap();
|
2022-10-02 18:16:10 -04:00
|
|
|
assert_eq!(fun.name(), MyElementImpl::class_name());
|
2022-10-02 20:27:01 -04:00
|
|
|
let element = MyElementImpl::create();
|
2022-10-02 18:16:10 -04:00
|
|
|
assert_eq!(
|
|
|
|
element.tag_name().to_uppercase(),
|
|
|
|
MyElementImpl::element_name().to_uppercase()
|
|
|
|
);
|
2022-10-02 20:27:01 -04:00
|
|
|
let document = window().unwrap().document().unwrap();
|
|
|
|
let body = document.body().unwrap();
|
|
|
|
|
|
|
|
// Test the connected callback
|
2022-10-05 20:31:29 -04:00
|
|
|
body.append_child(&element).unwrap();
|
2022-10-02 20:27:01 -04:00
|
|
|
assert_eq!(
|
|
|
|
element.text_content().unwrap(),
|
|
|
|
"Added a text node on connect"
|
|
|
|
);
|
|
|
|
|
|
|
|
// Test the disconnected callback
|
2022-10-05 20:31:29 -04:00
|
|
|
body.remove_child(&element).unwrap();
|
2022-10-02 20:27:01 -04:00
|
|
|
assert_eq!(element.text_content().unwrap(), "");
|
|
|
|
|
2022-10-05 20:31:29 -04:00
|
|
|
body.append_child(&element).unwrap();
|
2022-10-02 20:27:01 -04:00
|
|
|
element.set_attribute("class", "foo").unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
element.text_content().unwrap(),
|
|
|
|
"Setting class from None to foo"
|
|
|
|
);
|
|
|
|
|
|
|
|
// Test the adopted callback
|
|
|
|
// First we need a new window with a new document to perform the adoption with.
|
|
|
|
let new_window = window().unwrap().open().unwrap().unwrap();
|
|
|
|
// Then we can have the new document adopt this node.
|
|
|
|
new_window.document().unwrap().adopt_node(&element).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
element.text_content().unwrap(),
|
|
|
|
"Added a text node on adopt"
|
|
|
|
);
|
2022-10-02 18:16:10 -04:00
|
|
|
}
|
2022-10-06 22:13:18 -04:00
|
|
|
|
|
|
|
#[wasm_bindgen_test]
|
|
|
|
fn test_component_no_element_name() {
|
|
|
|
#[web_component(class_name = "AnElement")]
|
|
|
|
pub struct AnElement {}
|
|
|
|
impl WebComponentBinding for AnElement {}
|
|
|
|
|
|
|
|
assert_eq!(AnElement::element_name(), "an-element");
|
|
|
|
}
|
2022-10-02 18:16:10 -04:00
|
|
|
}
|