Lifecycle callbacks

This commit is contained in:
Jeremy Wall 2022-10-02 20:27:01 -04:00
parent 793a949053
commit ef59c1769c
2 changed files with 176 additions and 56 deletions

View File

@ -27,6 +27,8 @@ features = [
"Event", "Event",
"EventTarget", "EventTarget",
"Element", "Element",
"Node",
"Text",
"HtmlBaseElement", "HtmlBaseElement",
"HtmlElement", "HtmlElement",
"HtmlTemplateElement", "HtmlTemplateElement",

View File

@ -1,75 +1,167 @@
use js_sys::Function; use js_sys::Function;
use wasm_bindgen::{convert::IntoWasmAbi, prelude::*, JsCast, JsValue}; use wasm_bindgen::{convert::IntoWasmAbi, prelude::*, JsCast, JsValue};
use web_sys::window; use web_sys::{window, Element, HtmlElement};
type Result<T> = std::result::Result<T, JsValue>; type Result<T> = std::result::Result<T, JsValue>;
pub trait CustomElementImpl: IntoWasmAbi { // 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.
fn class_name() -> &'static str; pub trait CustomElementImpl: IntoWasmAbi {}
fn element_name() -> &'static str;
fn construct() -> Self; pub struct WebComponentHandle<T: CustomElementImpl> {
}
pub struct WebComponentHandle<T: CustomElementImpl + 'static> {
pub impl_handle: Closure<dyn FnMut() -> T>, pub impl_handle: Closure<dyn FnMut() -> T>,
pub element_constructor: Function, pub element_constructor: Function,
} }
pub fn define_web_component<T>() -> Result<WebComponentHandle<T>>
where
T: CustomElementImpl + 'static,
{
let body = format!(
"class {name} extends HTMLElement {{
constructor() {{
super();
//this.impl = impl();
}}
}}
customElements.define(\"{element_name}\", {name});
var element = customElements.get(\"{element_name}\");
return element;",
name = T::class_name(),
element_name = T::element_name(),
);
let fun = Function::new_with_args("impl", &body);
let f: Box<dyn FnMut() -> T> = Box::new(|| T::construct());
// TODO(jwall): Check the lifetimes on this guy.
let constructor_handle = Closure::wrap(f);
let element = fun
.call1(
&window().unwrap(),
constructor_handle.as_ref().unchecked_ref::<Function>(),
)?
.dyn_into()?;
Ok(WebComponentHandle {
element_constructor: element,
impl_handle: constructor_handle,
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test;
use web_sys::Text;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen] #[wasm_bindgen]
pub struct MyElementImpl(); #[derive(Default, Debug)]
pub struct MyElementImpl {}
impl CustomElementImpl for MyElementImpl { impl CustomElementImpl for MyElementImpl {}
fn class_name() -> &'static str {
impl MyElementImpl {
pub fn class_name() -> &'static str {
"MyElement" "MyElement"
} }
fn element_name() -> &'static str { pub fn element_name() -> &'static str {
"my-element" "my-element"
} }
fn construct() -> Self { pub fn define() -> Result<WebComponentHandle<Self>> {
Self() let registry = window().unwrap().custom_elements();
let maybe_element = registry.get(Self::element_name());
if maybe_element.is_truthy() {
return Err("Custom Element has already been defined".into());
}
let body = format!(
"class {name} extends HTMLElement {{
constructor() {{
super();
this._impl = impl();
}}
connectedCallback() {{
this._impl.connected_impl(this);
}}
disconnectedCallback() {{
this._impl.disconnected_impl(this);
}}
static get observedAttributes() {{
console.log('observed attributes: ', attrs);
return attrs;
}}
adoptedCallback() {{
console.log('In adoptedCallback');
this._impl.adopted_impl(this);
}}
attributeChangedCallback(name, oldValue, newValue) {{
this._impl.attribute_changed_impl(this, name, oldValue, newValue);
}}
}}
customElements.define(\"{element_name}\", {name});
var element = customElements.get(\"{element_name}\");
return element;",
name = Self::class_name(),
element_name = Self::element_name(),
);
let fun = Function::new_with_args("impl, attrs", &body);
let f: Box<dyn FnMut() -> Self> = Box::new(|| {
let obj = Self::new();
obj
});
let constructor_handle = Closure::wrap(f);
let element = fun
.call2(
&window().unwrap(),
constructor_handle.as_ref().unchecked_ref::<Function>(),
&Self::observed_attributes(),
)?
.dyn_into()?;
Ok(WebComponentHandle {
element_constructor: element,
impl_handle: constructor_handle,
})
}
}
#[wasm_bindgen]
impl MyElementImpl {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
}
#[wasm_bindgen]
pub fn create() -> Element {
window()
.unwrap()
.document()
.unwrap()
.create_element(Self::element_name())
.unwrap()
}
#[wasm_bindgen]
pub fn connected_impl(&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_with_val("element: ".to_owned(), element);
}
#[wasm_bindgen]
pub fn disconnected_impl(&self, element: &HtmlElement) {
log("Firing discconnected call back".to_owned());
let node = element.first_child().unwrap();
element.remove_child(&node).unwrap();
}
#[wasm_bindgen]
pub fn adopted_impl(&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);
}
pub fn observed_attributes() -> js_sys::Array {
let attrs = js_sys::Array::new();
attrs.push(&JsValue::from_str("class"));
attrs
}
#[wasm_bindgen]
pub fn attribute_changed_impl(
&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);
} }
} }
@ -81,21 +173,47 @@ mod tests {
pub fn log_with_val(message: String, val: &JsValue); pub fn log_with_val(message: String, val: &JsValue);
} }
// 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.
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_component_definition() { fn test_component() {
let obj = define_web_component::<MyElementImpl>().expect("Failed to define web component"); let obj = MyElementImpl::define().expect("Failed to define web component");
let fun = obj.element_constructor.dyn_ref::<Function>().unwrap(); let fun = obj.element_constructor.dyn_ref::<Function>().unwrap();
assert_eq!(fun.name(), MyElementImpl::class_name()); assert_eq!(fun.name(), MyElementImpl::class_name());
let element = MyElementImpl::create();
let element = window()
.unwrap()
.document()
.unwrap()
.create_element(MyElementImpl::element_name())
.unwrap();
assert_eq!( assert_eq!(
element.tag_name().to_uppercase(), element.tag_name().to_uppercase(),
MyElementImpl::element_name().to_uppercase() MyElementImpl::element_name().to_uppercase()
); );
let document = window().unwrap().document().unwrap();
let body = document.body().unwrap();
// Test the connected callback
let node = body.append_child(element.as_ref()).unwrap();
assert_eq!(
element.text_content().unwrap(),
"Added a text node on connect"
);
// Test the disconnected callback
body.remove_child(&node).unwrap();
assert_eq!(element.text_content().unwrap(), "");
body.append_child(element.as_ref()).unwrap();
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"
);
} }
} }