Compare commits

...

20 Commits
v0.1.1 ... main

Author SHA1 Message Date
8e11072b52 fix: missing s in base_clas 2023-12-31 09:10:20 -06:00
aaea07a7b8 Update docs for the new attribute base_class 2023-12-08 19:30:28 -05:00
5ba459bcb6 Allow you to configure which element you inherit from. 2023-12-08 19:24:38 -05:00
2b00383712 Cleanup some TODOS 2023-12-08 18:48:41 -05:00
a8d99c6284 Add tests for the mutable variants. 2023-11-25 21:49:07 -05:00
f03d1641d9 Add an attach shadow method 2023-11-25 21:09:12 -05:00
a63670b7d3 Allow mutable forms of the API 2023-11-21 23:03:41 -05:00
7b589373ed Fix some ambiguous wasm_bindgen crate references. 2023-11-21 19:44:14 -05:00
8ce79d34a5 Add the observed event handlers to the shadowRoot
If it exists then add it there. Otherwise add it to the custom element itself.
2023-11-15 21:00:45 -05:00
bb60556f29 docs: Update docs with event handling information. 2023-11-13 18:50:24 -05:00
f41a9d3100 bug: Fix some bugs in the init wiring 2023-11-13 18:46:38 -05:00
76baa1871e feat: Add proper event listener support 2023-11-13 18:07:24 -05:00
8956f485a8 feat: init hook for element constructor 2023-11-13 17:54:14 -05:00
c39293636d maint: bump version to v0.2.0 2023-11-12 20:04:08 -05:00
7f25fae4dc Doc cleanup and expansion 2023-11-12 20:00:34 -05:00
2f7c3793c5 Add an api for retrieving the id of the element 2023-11-12 18:29:24 -05:00
e71a28b4e3 Add an api for TemplateElement definition. 2023-11-12 18:00:01 -05:00
f5791b9355 Add Makefile for convenience 2023-10-25 17:18:03 -04:00
9dbdccef78 Construct the ONCE name in a way that satisfies lint 2023-10-25 17:17:29 -04:00
55d743f01c fix: usings and lint stuff found by using in app 2023-09-23 12:13:42 -04:00
5 changed files with 490 additions and 58 deletions

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
build:
cargo build;
test:
cd wasm-web-component; wasm-pack test --headless --firefox

View File

@ -1,6 +1,6 @@
[package] [package]
name = "wasm-web-component-macros" name = "wasm-web-component-macros"
version = "0.1.1" version = "0.3.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -17,3 +17,6 @@ str_inflector = "0.12.0"
[dependencies.syn] [dependencies.syn]
version = "1.0.101" version = "1.0.101"
features = ["full"] features = ["full"]
[features]
HtmlTemplateElement = []

View File

@ -33,13 +33,23 @@ fn expand_crate_ref(name: &str, path: Path) -> syn::Path {
} }
} }
struct AttributeConfig {
class_name: Literal,
element_name: Literal,
observed_attributes: Literal,
observed_events: Literal,
base_class: Literal,
}
fn get_class_and_element_names( fn get_class_and_element_names(
args: Vec<NestedMeta>, args: Vec<NestedMeta>,
struct_name: &Ident, struct_name: &Ident,
) -> (Literal, Literal, Literal) { ) -> AttributeConfig {
let mut class_name = None; let mut class_name = None;
let mut element_name = None; let mut element_name = None;
let mut observed_attributes = None; let mut observed_attributes = None;
let mut observed_events = None;
let mut base_class = None;
for arg in args { for arg in args {
if let NestedMeta::Meta(Meta::NameValue(nv)) = arg { if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
if nv.path.is_ident("class_name") { if nv.path.is_ident("class_name") {
@ -54,6 +64,14 @@ fn get_class_and_element_names(
if let Lit::Str(nm) = nv.lit { if let Lit::Str(nm) = nv.lit {
observed_attributes = Some(nm); observed_attributes = Some(nm);
} }
} else if nv.path.is_ident("observed_events") {
if let Lit::Str(nm) = nv.lit {
observed_events = Some(nm);
}
} else if nv.path.is_ident("base_class") {
if let Lit::Str(nm) = nv.lit {
base_class = Some(nm);
}
} }
} }
} }
@ -65,15 +83,25 @@ fn get_class_and_element_names(
let element_name = match element_name.map(|n| n.token()) { let element_name = match element_name.map(|n| n.token()) {
Some(n) => n, Some(n) => n,
None => { None => {
let class_kebab = class_name.to_string().to_kebab_case(); let class_kebab = class_name.to_string().to_kebab_case().to_lowercase();
LitStr::new(&class_kebab, Span::call_site()).token() LitStr::new(&class_kebab, Span::call_site()).token()
} }
}; };
let base_class = base_class.unwrap_or_else(|| LitStr::new("HTMLElement", Span::call_site())).token();
let observed_attributes = observed_attributes let observed_attributes = observed_attributes
.map(|n| n.token()) .map(|n| n.token())
.unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token()); .unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token());
(class_name, element_name, observed_attributes) let observed_events = observed_events
.map(|n| n.token())
.unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token());
AttributeConfig {
class_name,
element_name,
observed_attributes,
observed_events,
base_class,
}
} }
fn expand_component_def( fn expand_component_def(
@ -96,7 +124,18 @@ fn expand_component_def(
} }
} }
fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_attrs: Literal) -> syn::ItemImpl { fn expand_wc_struct_trait_shim(
struct_name: &Ident,
once_name: &Ident,
config: AttributeConfig,
) -> syn::ItemImpl {
let AttributeConfig {
class_name: _,
element_name: _,
observed_attributes,
observed_events,
base_class,
} = config;
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef)); let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef));
let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle)); let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle));
parse_quote! { parse_quote! {
@ -115,21 +154,32 @@ fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_att
let _ = Self::define(); let _ = Self::define();
}); });
} }
#[doc = "Defines this web component element if not defined already otherwise returns an error."] #[doc = "Defines this web component element if not defined already otherwise returns an error."]
pub fn define() -> std::result::Result<#handle_path, JsValue> { pub fn define() -> std::result::Result<#handle_path, ::wasm_bindgen::JsValue> {
use wasm_bindgen::JsCast; use ::wasm_bindgen::JsCast;
use web_sys::{window, Element, HtmlElement}; use web_sys::{Element, HtmlElement};
let registry = web_sys::window().unwrap().custom_elements(); let registry = web_sys::window().unwrap().custom_elements();
let maybe_element = registry.get(Self::element_name()); let maybe_element = registry.get(Self::element_name());
if maybe_element.is_truthy() { if maybe_element.is_truthy() {
return Err("Custom Element has already been defined".into()); return Err("Custom Element has already been defined".into());
} }
let body = format!( let body = format!(
"class {name} extends HTMLElement {{ "class {name} extends {base_class} {{
constructor() {{ constructor() {{
super(); super();
this._impl = impl(); this._impl = impl();
this._impl.init_impl(this);
var self = this;
if (self.shadowRoot) {{
for (const t of this.observedEvents()) {{
self.shadowRoot.addEventListener(t, function(evt) {{ self.handleComponentEvent(evt); }} );
}}
}} else {{
for (const t of self.observedEvents()) {{
self.addEventListener(t, function(evt) {{ self.handleComponentEvent(evt); }} );
}}
}}
}} }}
connectedCallback() {{ connectedCallback() {{
@ -144,11 +194,15 @@ fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_att
return {observed_attributes}; return {observed_attributes};
}} }}
observedEvents() {{
return {observed_events};
}}
adoptedCallback() {{ adoptedCallback() {{
this._impl.adopted_impl(this); this._impl.adopted_impl(this);
}} }}
attributeChangedCallback(name, oldValue, newValue) {{ attributeChangedCallback(name, oldValue, newValue) {{
this._impl.attribute_changed_impl(this, name, oldValue, newValue); this._impl.attribute_changed_impl(this, name, oldValue, newValue);
}} }}
@ -161,17 +215,19 @@ var element = customElements.get(\"{element_name}\");
return element;", return element;",
name = Self::class_name(), name = Self::class_name(),
element_name = Self::element_name(), element_name = Self::element_name(),
observed_attributes = #observed_attrs, observed_attributes = #observed_attributes,
observed_events = #observed_events,
base_class = #base_class,
); );
let fun = js_sys::Function::new_with_args("impl", &body); let fun = js_sys::Function::new_with_args("impl", &body);
let f: Box<dyn FnMut() -> Self> = Box::new(|| { let f: Box<dyn FnMut() -> Self> = Box::new(|| {
let obj = Self::new(); let obj = Self::new();
obj obj
}); });
let constructor_handle = wasm_bindgen::prelude::Closure::wrap(f).into_js_value().unchecked_into::<js_sys::Function>(); let constructor_handle = ::wasm_bindgen::prelude::Closure::wrap(f).into_js_value().unchecked_into::<js_sys::Function>();
let element = fun let element = fun
.call1( .call1(
&window().unwrap(), &web_sys::window().unwrap(),
constructor_handle.as_ref(), constructor_handle.as_ref(),
)? )?
.dyn_into()?; .dyn_into()?;
@ -186,47 +242,72 @@ return element;",
fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl { fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl {
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentBinding)); let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentBinding));
parse_quote! { parse_quote! {
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
impl #struct_name { impl #struct_name {
#[wasm_bindgen::prelude::wasm_bindgen(constructor)] #[::wasm_bindgen::prelude::wasm_bindgen(constructor)]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
pub fn connected_impl(&self, element: &web_sys::HtmlElement) { #[doc = "Attach an open shadowroot to our element."]
pub fn attach_shadow(&self, element: &web_sys::HtmlElement, root: &str) {
self.attach_shadow_with_mode(element, root, web_sys::ShadowRootMode::Open);
}
#[::wasm_bindgen::prelude::wasm_bindgen]
#[doc = "Attach a shadowroot with the given mode to our element."]
pub fn attach_shadow_with_mode(&self, element: &web_sys::HtmlElement, root: &str, mode: web_sys::ShadowRootMode) {
let shadow_root = element.attach_shadow(&web_sys::ShadowRootInit::new(mode)).unwrap();
shadow_root.set_inner_html(root);
}
#[::wasm_bindgen::prelude::wasm_bindgen]
pub fn init_impl(&mut self, element: &web_sys::HtmlElement) {
use #trait_path;
self.init(element);
self.init_mut(element);
}
#[::wasm_bindgen::prelude::wasm_bindgen]
pub fn connected_impl(&mut self, element: &web_sys::HtmlElement) {
use #trait_path; use #trait_path;
self.connected(element); self.connected(element);
self.connected_mut(element);
} }
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
pub fn disconnected_impl(&self, element: &web_sys::HtmlElement) { pub fn disconnected_impl(&mut self, element: &web_sys::HtmlElement) {
use #trait_path; use #trait_path;
self.disconnected(element); self.disconnected(element);
self.disconnected_mut(element);
} }
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
pub fn adopted_impl(&self, element: &web_sys::HtmlElement) { pub fn adopted_impl(&mut self, element: &web_sys::HtmlElement) {
use #trait_path; use #trait_path;
self.adopted(element); self.adopted(element);
self.adopted_mut(element);
} }
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
pub fn attribute_changed_impl( pub fn attribute_changed_impl(
&self, &mut self,
element: &web_sys::HtmlElement, element: &web_sys::HtmlElement,
name: wasm_bindgen::JsValue, name: ::wasm_bindgen::JsValue,
old_value: wasm_bindgen::JsValue, old_value: ::wasm_bindgen::JsValue,
new_value: wasm_bindgen::JsValue, new_value: ::wasm_bindgen::JsValue,
) { ) {
use #trait_path; use #trait_path;
self.attribute_changed(element, name, old_value, new_value); self.attribute_changed(element, name.clone(), old_value.clone(), new_value.clone());
self.attribute_changed_mut(element, name, old_value, new_value);
} }
pub fn handle_component_event_impl(&self, element: &web_sys::HtmlElement, event: &web_sys::Event) { pub fn handle_component_event_impl(&mut self, element: &web_sys::HtmlElement, event: &web_sys::Event) {
use #trait_path; use #trait_path;
self.handle_event(element, event); self.handle_event(element, event);
self.handle_event_mut(element, event);
} }
} }
} }
@ -239,22 +320,24 @@ fn expand_binding(struct_name: &Ident) -> syn::ItemImpl {
) )
} }
fn expand_struct( fn expand_web_component_struct(
item_struct: ItemStruct, item_struct: ItemStruct,
class_name: Literal, config: AttributeConfig,
element_name: Literal,
observed_attributes: Literal,
) -> TokenStream { ) -> TokenStream {
let struct_name = item_struct.ident.clone(); let struct_name = item_struct.ident.clone();
let struct_once_name = Ident::new(&(struct_name.to_string() + "Once"), Span::call_site()); let struct_once_name = Ident::new(
let component_def = expand_component_def(&struct_name, &class_name, &element_name); &(struct_name.to_string().to_snake_case().to_uppercase() + "_ONCE"),
let non_wasm_impl = expand_struct_trait_shim(&struct_name, &struct_once_name, observed_attributes); Span::call_site(),
);
let component_def = expand_component_def(&struct_name, &config.class_name, &config.element_name);
let non_wasm_impl =
expand_wc_struct_trait_shim(&struct_name, &struct_once_name, config);
let wasm_shim = expand_wasm_shim(&struct_name); let wasm_shim = expand_wasm_shim(&struct_name);
let binding_trait = expand_binding(&struct_name); let binding_trait = expand_binding(&struct_name);
let expanded = quote! { let expanded = quote! {
use std::sync::Once; #[allow(non_snake_case)]
static #struct_once_name: Once = Once::new(); static #struct_once_name: std::sync::Once = std::sync::Once::new();
#[wasm_bindgen::prelude::wasm_bindgen] #[::wasm_bindgen::prelude::wasm_bindgen]
#[derive(Default, Debug)] #[derive(Default, Debug)]
#item_struct #item_struct
#component_def #component_def
@ -266,16 +349,60 @@ fn expand_struct(
TokenStream::from(expanded) TokenStream::from(expanded)
} }
#[cfg(feature = "HtmlTemplateElement")]
fn expand_template_struct(item_struct: ItemStruct) -> TokenStream {
let struct_name = item_struct.ident.clone();
let struct_once_name = Ident::new(
&(struct_name.to_string().to_snake_case().to_uppercase() + "_ONCE"),
Span::call_site(),
);
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(TemplateElement));
let expanded = quote! {
use web_sys::Node;
static #struct_once_name: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
#item_struct
impl #trait_path for #struct_name {}
impl #struct_name {
#[doc = "Defines this HtmlTemplateElement and adds it to the document exactly once. Subsequent calls are noops. Returns the the template element id it exists on the template element."]
pub fn define_once() -> Option<&'static Option<String>> {
#struct_once_name.get_or_init(|| {
let template_element = Self::render();
let id: Option<String> = template_element.get_attribute("id");
let body = web_sys::window().expect("Failed to get window")
.document().expect("Failed to get window document").
body().expect("Failed to get document body");
body.append_child(template_element.as_ref()).expect("Failed to add template element to document");
return id;
});
return #struct_once_name.get();
}
#[doc = "Returns the the template element id it exists. None if the element has not been defined yet. Some(&None) if the element has no id. Some(&Some(id)) if the element has an id."]
pub fn get_id() -> Option<&'static Option<String>> {
return #struct_once_name.get();
}
}
};
TokenStream::from(expanded)
}
/// Creates the necessary Rust and Javascript shims for a Web Component. /// Creates the necessary Rust and Javascript shims for a Web Component.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
// TODO(jwall): Attrs for class name and element name
// Gather our attributes // Gather our attributes
let args = parse_macro_input!(attr as AttributeArgs); let args = parse_macro_input!(attr as AttributeArgs);
let item_struct = parse_macro_input!(item as ItemStruct); let item_struct = parse_macro_input!(item as ItemStruct);
let (class_name, element_name, observed_attributes) = let config =
get_class_and_element_names(args, &item_struct.ident); get_class_and_element_names(args, &item_struct.ident);
expand_struct(item_struct, class_name, element_name, observed_attributes) expand_web_component_struct(item_struct, config)
}
/// Creates the neccessary Rust and Javascript shims for rendering an HtmlTemplateElement
#[cfg(feature = "HtmlTemplateElement")]
#[proc_macro_attribute]
pub fn template_element(_attr: TokenStream, item: TokenStream) -> TokenStream {
let item_struct = parse_macro_input!(item as ItemStruct);
expand_template_struct(item_struct)
} }

View File

@ -1,22 +1,21 @@
[package] [package]
name = "wasm-web-component" name = "wasm-web-component"
version = "0.1.1" version = "0.3.0"
edition = "2021" edition = "2021"
author = "Jeremy Wall <jeremy@marzhillstudios.com>"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies.wasm-web-component-macros]
wasm-web-component-macros = { path = "../macros" } path = "../macros"
[dependencies.wasm-bindgen-test] [dependencies.wasm-bindgen-test]
version = "0.3" version = "0.3"
[dependencies.wasm-bindgen] [dependencies.wasm-bindgen]
version = "= 0.2.81" version = "~0.2"
[dependencies.js-sys] [dependencies.js-sys]
version = "0.3" version = "0.3"
@ -25,9 +24,8 @@ version = "0.3"
version = "0.3" version = "0.3"
features = [ features = [
"CustomElementRegistry", "CustomElementRegistry",
"CustomEvent",
"Document", "Document",
#"DocumentFragment",
"KeyboardEvent",
"Event", "Event",
"EventTarget", "EventTarget",
"Element", "Element",
@ -35,7 +33,6 @@ features = [
"Text", "Text",
"HtmlBaseElement", "HtmlBaseElement",
"HtmlElement", "HtmlElement",
"HtmlTemplateElement",
"HtmlSlotElement", "HtmlSlotElement",
"Node", "Node",
"ShadowRoot", "ShadowRoot",
@ -44,3 +41,10 @@ features = [
"Window", "Window",
"console" "console"
] ]
[features]
default = ["HtmlTemplateElement"]
HtmlTemplateElement = [
"web-sys/HtmlTemplateElement",
"wasm-web-component-macros/HtmlTemplateElement",
]

View File

@ -1,5 +1,8 @@
use js_sys::Function; use js_sys::Function;
use wasm_bindgen::JsCast;
use wasm_bindgen::{convert::IntoWasmAbi, JsValue}; use wasm_bindgen::{convert::IntoWasmAbi, JsValue};
#[cfg(feature = "HtmlTemplateElement")]
use web_sys::HtmlTemplateElement;
use web_sys::{window, Element, Event, HtmlElement, Window}; use web_sys::{window, Element, Event, HtmlElement, Window};
/// This attribute proc-macro will generate the following trait implementations /// This attribute proc-macro will generate the following trait implementations
@ -14,11 +17,121 @@ use web_sys::{window, Element, Event, HtmlElement, Window};
/// It supports three optional attributes `name = value` parameters. /// It supports three optional attributes `name = value` parameters.
/// * `class_name = "ClassName"` - The class name to use for the javascript shim. If not provided uses the structs name instead. /// * `class_name = "ClassName"` - The class name to use for the javascript shim. If not provided uses the structs name instead.
/// * `element_name = "class-name"` - A valid custom element name to use for the element. if not proviced derives it from the class name. /// * `element_name = "class-name"` - A valid custom element name to use for the element. if not proviced derives it from the class name.
/// * `observed_attrs = "['attr1', attr2']"` - A javascript array with a list of observed attributes for this compoment. Defaults to "[]". /// * `observed_attrs = "['attr1', 'attr2']"` - A javascript array with a list of observed attributes for this compoment. Defaults to "[]".
/// * `observed_events = "['click', 'change']"` - A javascript array with a list of observed event types for this compoment. Defaults to "[]".
/// * `base_class = "HTMLInputElement"` - The HTMLElement base class this custom-element should
/// inherit from. Defaults to "HTMLElement".
/// ///
/// It will also create a `Self::define_once` method that will define the WebComponent exactly
/// once.
///
/// ## Example
///
/// ```ignore
/// use web_sys::*;
/// use wasm_bindgen::*;
/// use wasm_web_component::{web_component, WebComponent, WebComponentHandle, WebComponentDef, WebComponentBinding};
///
/// #[web_component(
/// class_name = "MyElement",
/// element_name = "my-element",
/// observed_attrs = "['class']",
/// observed_events = "['click']",
/// base_class = "HTMLElement"
/// )]
/// pub struct MyElementImpl {}
///
/// impl WebComponentBinding for MyElementImpl {
/// fn connected(&self, element: &HtmlElement) {
/// let node = Text::new().unwrap();
/// node.set_text_content(Some("Added a text node on connect".into()));
/// element.append_child(&node).unwrap();
/// }
///
/// fn disconnected(&self, element: &HtmlElement) {
/// let node = element.first_child().unwrap();
/// element.remove_child(&node).unwrap();
/// }
///
/// fn adopted(&self, element: &HtmlElement) {
/// let node = Text::new().unwrap();
/// node.set_text_content(Some("Added a text node on adopt".into()));
/// element.append_child(&node).unwrap();
/// }
///
/// fn attribute_changed(
/// &self,
/// element: &HtmlElement,
/// name: JsValue,
/// old_value: JsValue,
/// new_value: JsValue,
/// ) {
/// 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();
/// }
///
/// fn handle_event(&self, element: &HtmlElement, event: &Event) {
/// // handle this event
/// }
/// }
///
/// pub fn define_me() {
/// MyElementImpl::define_once();
/// }
/// ```
/// Reference [MDN Web Components Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components) /// Reference [MDN Web Components Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
pub use wasm_web_component_macros::web_component; pub use wasm_web_component_macros::web_component;
/// This attribute proc-macro will generate the following trait implementation
/// [TemplateElement](trait@TemplateElement)
///
/// It will also generate a wasm_bindgen compatible impl block for your struct. It expects
/// you to implement [TemplateElementRender](trait@TemplateElementRender) trait in order to
/// allow it to implement the methods using methods from that trait.
///
/// You can define the template element exactly once by calling the `Self::define_once` method.
/// Subsequent calls to that method will be a noop. It returns one of the following values:
/// * `Some(None)` If the template doesn't have an id.
/// * `Some(Some(id))` If the template has an id.
/// * `None` Should never get returned.
///
/// A `get_id` method will also get defined for you that returns the same values with the difference that
/// if the template has not been defined yet `None` will get returned.
///
/// ## Example usage
/// ```ignore
/// use wasm_web_component::*;
/// use wasm_bindgen::*;
/// # #[cfg(feature = "HtmlTemplateElement")]
/// #[template_element]
/// pub struct MyTemplate ();
/// impl TemplateElementRender for MyTemplate {
/// fn render() -> HtmlTemplateElement {
/// let val: JsValue = window()
/// .unwrap()
/// .document()
/// .unwrap()
/// .create_element("template")
/// .unwrap()
/// .into();
/// let el: HtmlTemplateElement = val.into();
/// el.set_attribute("id", "template-id").unwrap();
/// return el;
/// }
/// }
///
/// pub fn define_it() {
/// let id: Option<&'static Option<String>> = MyTemplate::define_once();
/// }
/// ```
pub use wasm_web_component_macros::template_element;
/// Helper trait for Rust Web Components. This is autogenerated /// Helper trait for Rust Web Components. This is autogenerated
/// by the [`#[web_component]`](web_component) attribute. /// by the [`#[web_component]`](web_component) attribute.
pub trait WebComponentDef: IntoWasmAbi + Default { pub trait WebComponentDef: IntoWasmAbi + Default {
@ -27,15 +140,20 @@ pub trait WebComponentDef: IntoWasmAbi + Default {
} }
fn create() -> Element { fn create() -> Element {
Self::create_in_window(window().unwrap()) Self::create_in_window(window().expect("Failed to get window"))
} }
fn create_in_window(window: Window) -> Element { fn create_in_window(window: Window) -> Element {
window window
.document() .document()
.unwrap() .expect("Failed to get document")
.create_element(Self::element_name()) .create_element(Self::element_name())
.unwrap() .expect("Failed to create element")
}
/// Creates a custom event
fn custom_event(event_type: &str) -> web_sys::Event {
web_sys::CustomEvent::new(event_type).unwrap().dyn_into().unwrap()
} }
fn element_name() -> &'static str; fn element_name() -> &'static str;
@ -46,6 +164,15 @@ pub trait WebComponentDef: IntoWasmAbi + Default {
/// Each method is optional. You only need to implement the ones /// Each method is optional. You only need to implement the ones
/// you want to specify behavior for. /// you want to specify behavior for.
pub trait WebComponentBinding: WebComponentDef { pub trait WebComponentBinding: WebComponentDef {
/// Called during element construction.
fn init(&self, _element: &HtmlElement) {
// noop
}
fn init_mut(&mut self, _element: &HtmlElement) {
// noop
}
/// Called when the web component is connected to the DOM. /// Called when the web component is connected to the DOM.
/// This is when you should do any setup like attaching a ShadowDom /// This is when you should do any setup like attaching a ShadowDom
/// or appending elements. /// or appending elements.
@ -53,16 +180,33 @@ pub trait WebComponentBinding: WebComponentDef {
// noop // noop
} }
/// Called when the web component is connected to the DOM.
/// This is when you should do any setup like attaching a ShadowDom
/// or appending elements.
fn connected_mut(&mut self, _element: &HtmlElement) {
// noop
}
/// Called when the web component is disconnected from the DOM. /// Called when the web component is disconnected from the DOM.
fn disconnected(&self, _element: &HtmlElement) { fn disconnected(&self, _element: &HtmlElement) {
// noop // noop
} }
/// Called when the web component is disconnected from the DOM.
fn disconnected_mut(&mut self, _element: &HtmlElement) {
// noop
}
/// Called When the web component is moved to a new document. /// Called When the web component is moved to a new document.
fn adopted(&self, _element: &HtmlElement) { fn adopted(&self, _element: &HtmlElement) {
// noop // noop
} }
/// Called When the web component is moved to a new document.
fn adopted_mut(&mut self, _element: &HtmlElement) {
// noop
}
/// Called when one of the observed attributes has changed. /// Called when one of the observed attributes has changed.
/// the observedc attributes are listed in the observed_attrs argument to the /// the observedc attributes are listed in the observed_attrs argument to the
/// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute. /// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute.
@ -76,16 +220,46 @@ pub trait WebComponentBinding: WebComponentDef {
// noop // noop
} }
/// Top level event handler for this custome element. /// Called when one of the observed attributes has changed.
/// the observedc attributes are listed in the observed_attrs argument to the
/// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute.
fn attribute_changed_mut(
&mut self,
_element: &HtmlElement,
_name: JsValue,
_old_value: JsValue,
_new_value: JsValue,
) {
// noop
}
/// Top level event handler for this custom element.
fn handle_event(&self, _element: &HtmlElement, _event: &Event) { fn handle_event(&self, _element: &HtmlElement, _event: &Event) {
// noop // noop
} }
/// Top level event handler for this custom element.
fn handle_event_mut(&mut self, _element: &HtmlElement, _event: &Event) {
// noop
}
} }
/// Marker trait used in the generated shims to assert that their are Rust implemtntations /// Marker trait used in the generated shims to assert that there are Rust implemtntations
/// of the callback functions for the component. /// of the callback functions for the component.
pub trait WebComponent: WebComponentBinding {} pub trait WebComponent: WebComponentBinding {}
/// Defines the template element rendering method.
#[cfg(feature = "HtmlTemplateElement")]
pub trait TemplateElementRender {
// Creates and returns an HtmlTemplateElement.
fn render() -> HtmlTemplateElement;
}
/// Marker trait used in the generated shims to assert that there are Rust implemtntations
/// of the rendering function for the component.
#[cfg(feature = "HtmlTemplateElement")]
pub trait TemplateElement: TemplateElementRender {}
/// A handle for your WebComponent Definition. Offers easy access to construct your /// A handle for your WebComponent Definition. Offers easy access to construct your
/// element. /// element.
pub struct WebComponentHandle { pub struct WebComponentHandle {
@ -191,7 +365,7 @@ mod tests {
#[web_component( #[web_component(
class_name = "MyElement", class_name = "MyElement",
element_name = "my-element", element_name = "my-element",
observed_attrs = "['class']" observed_attrs = "['class']",
)] )]
pub struct MyElementImpl {} pub struct MyElementImpl {}
@ -270,9 +444,96 @@ mod tests {
"Added a text node on adopt" "Added a text node on adopt"
); );
} }
// Then we can have the new document adopt this node.
} }
#[wasm_bindgen_test]
fn test_component_mut() {
#[web_component(
class_name = "MyElementMut",
element_name = "my-element-mut",
observed_attrs = "['class']",
)]
pub struct MyElementMutImpl {}
impl WebComponentBinding for MyElementMutImpl {
fn connected_mut(&mut self, element: &HtmlElement) {
let node = Text::new().unwrap();
node.set_text_content(Some("Added a text node on connect".into()));
element.append_child(&node).unwrap();
}
fn disconnected_mut(&mut self, element: &HtmlElement) {
let node = element.first_child().unwrap();
element.remove_child(&node).unwrap();
}
fn adopted_mut(&mut self, element: &HtmlElement) {
let node = Text::new().unwrap();
node.set_text_content(Some("Added a text node on adopt".into()));
element.append_child(&node).unwrap();
}
fn attribute_changed_mut(
&mut self,
element: &HtmlElement,
name: JsValue,
old_value: JsValue,
new_value: JsValue,
) {
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();
}
}
let obj = MyElementMutImpl::define().expect("Failed to define web component");
let fun = obj.element_constructor.dyn_ref::<Function>().unwrap();
assert_eq!(fun.name(), MyElementMutImpl::class_name());
let element = MyElementMutImpl::create();
assert_eq!(
element.tag_name().to_uppercase(),
MyElementMutImpl::element_name().to_uppercase()
);
let document = window().unwrap().document().unwrap();
let body = document.body().unwrap();
// Test the connected callback
body.append_child(&element).unwrap();
assert_eq!(
element.text_content().unwrap(),
"Added a text node on connect"
);
// Test the disconnected callback
body.remove_child(&element).unwrap();
assert_eq!(element.text_content().unwrap(), "");
body.append_child(&element).unwrap();
element.set_attribute("class", "foo").unwrap();
assert_eq!(
element.text_content().unwrap(),
"Setting class from None to foo"
);
// NOTE(jwall): If we are running headless then this can fail sometimes.
// We don't fail the test when that happens.
if let Ok(Some(new_window)) = window().unwrap().open() {
// Test the adopted callback
// First we need a new window with a new document to perform the adoption with.
new_window.document().unwrap().adopt_node(&element).unwrap();
assert_eq!(
element.text_content().unwrap(),
"Added a text node on adopt"
);
} else {
assert!(false);
}
}
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_component_no_element_name() { fn test_component_no_element_name() {
#[web_component(class_name = "AnElement")] #[web_component(class_name = "AnElement")]
@ -301,4 +562,36 @@ mod tests {
assert_eq!(ThisElement::class_name(), "ThisElement"); assert_eq!(ThisElement::class_name(), "ThisElement");
assert_eq!(ThisElement::element_name(), "this-old-element"); assert_eq!(ThisElement::element_name(), "this-old-element");
} }
// TODO(jwall): Tests for event handling
// TODO(jwall): Benchmarks for TemplateElements?
#[cfg(feature = "HtmlTemplateElement")]
#[wasm_bindgen_test]
fn test_template_element_render_once() {
use wasm_web_component_macros::template_element;
#[template_element]
pub struct MyTemplate();
impl TemplateElementRender for MyTemplate {
fn render() -> HtmlTemplateElement {
let val: JsValue = window()
.unwrap()
.document()
.unwrap()
.create_element("template")
.unwrap()
.into();
let el: HtmlTemplateElement = val.into();
el.set_attribute("id", "template-id").unwrap();
return el;
}
}
let body = window().unwrap().document().unwrap().body().unwrap();
assert!(!body.last_child().unwrap().has_type::<HtmlTemplateElement>());
let id = MyTemplate::define_once();
assert_eq!(id.unwrap(), &Some(String::from("template-id")));
assert!(body.last_child().unwrap().has_type::<HtmlTemplateElement>());
}
} }