175 lines
5.5 KiB
Rust
Raw Normal View History

2022-10-02 18:16:10 -04:00
use js_sys::Function;
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
pub mod macros;
2022-10-05 18:36:26 -04:00
pub trait WebComponentDef: IntoWasmAbi + Default {
fn new() -> Self {
Self::default()
}
2022-10-02 18:16:10 -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;
}
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
}
}
pub trait WebComponent: WebComponentBinding {}
// TODO(jwall): Trait methods can't be exported out to js yet so we'll need a wrapper object or we'll need to `Derive` this api in a prop-macro.
pub struct WebComponentHandle<T> {
pub impl_handle: Closure<dyn FnMut() -> T>,
pub element_constructor: Function,
2022-10-02 18:16:10 -04:00
}
#[cfg(test)]
mod tests {
use super::*;
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;
use web_sys::{window, HtmlElement};
2022-10-02 18:16:10 -04:00
use web_component_derive::web_component;
2022-10-02 18:16:10 -04:00
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2022-10-02 20:27:01 -04:00
#[web_component(
class_name = "MyElement",
element_name = "my-element",
observed_attrs = "['class']"
)]
pub struct MyElementImpl {}
2022-10-02 20:27:01 -04:00
impl WebComponentBinding for MyElementImpl {
fn connected(&self, element: &HtmlElement) {
2022-10-02 20:27:01 -04:00
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()
));
2022-10-02 20:27:01 -04:00
}
fn disconnected(&self, element: &HtmlElement) {
2022-10-02 20:27:01 -04:00
log("Firing discconnected call back".to_owned());
let node = element.first_child().unwrap();
element.remove_child(&node).unwrap();
}
fn adopted(&self, element: &HtmlElement) {
2022-10-02 20:27:01 -04:00
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(
2022-10-02 20:27:01 -04:00
&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 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() {
let obj = MyElementImpl::define().expect("Failed to define web component");
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
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
body.remove_child(&element).unwrap();
2022-10-02 20:27:01 -04:00
assert_eq!(element.text_content().unwrap(), "");
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
}
}