From e71a28b4e3946b96210313d8ac275cb16dd67a75 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 12 Nov 2023 17:29:00 -0500 Subject: [PATCH] Add an api for TemplateElement definition. --- macros/Cargo.toml | 3 ++ macros/src/lib.rs | 58 +++++++++++++++++++++++++++++++---- wasm-web-component/Cargo.toml | 14 ++++++--- wasm-web-component/src/lib.rs | 44 +++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 12 deletions(-) diff --git a/macros/Cargo.toml b/macros/Cargo.toml index a12c86a..f6fc340 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -17,3 +17,6 @@ str_inflector = "0.12.0" [dependencies.syn] version = "1.0.101" features = ["full"] + +[features] +HtmlTemplateElement = [] diff --git a/macros/src/lib.rs b/macros/src/lib.rs index cec6700..8fc3117 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -96,7 +96,11 @@ 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, + observed_attrs: Literal, +) -> syn::ItemImpl { let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef)); let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle)); parse_quote! { @@ -115,7 +119,7 @@ fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_att let _ = Self::define(); }); } - + #[doc = "Defines this web component element if not defined already otherwise returns an error."] pub fn define() -> std::result::Result<#handle_path, wasm_bindgen::JsValue> { use wasm_bindgen::JsCast; @@ -239,16 +243,20 @@ fn expand_binding(struct_name: &Ident) -> syn::ItemImpl { ) } -fn expand_struct( +fn expand_web_component_struct( item_struct: ItemStruct, class_name: Literal, element_name: Literal, observed_attributes: Literal, ) -> 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 struct_once_name = Ident::new( + &(struct_name.to_string().to_snake_case().to_uppercase() + "_ONCE"), + Span::call_site(), + ); let component_def = expand_component_def(&struct_name, &class_name, &element_name); - let non_wasm_impl = expand_struct_trait_shim(&struct_name, &struct_once_name, observed_attributes); + let non_wasm_impl = + expand_wc_struct_trait_shim(&struct_name, &struct_once_name, observed_attributes); let wasm_shim = expand_wasm_shim(&struct_name); let binding_trait = expand_binding(&struct_name); let expanded = quote! { @@ -268,6 +276,36 @@ fn expand_struct( 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 std::sync::Once; + use web_sys::Node; + static #struct_once_name: Once = Once::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."] + pub fn define_once() { // TODO(jwall): Should this return the element? + #struct_once_name.call_once(|| { + let template_element = Self::render(); + 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"); + }) + } + } + }; + TokenStream::from(expanded) +} + /// Creates the necessary Rust and Javascript shims for a Web Component. #[proc_macro_attribute] pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -279,5 +317,13 @@ pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { let (class_name, element_name, observed_attributes) = 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, class_name, element_name, observed_attributes) +} + +/// 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) } diff --git a/wasm-web-component/Cargo.toml b/wasm-web-component/Cargo.toml index 343ce77..7403a9d 100644 --- a/wasm-web-component/Cargo.toml +++ b/wasm-web-component/Cargo.toml @@ -2,15 +2,14 @@ name = "wasm-web-component" version = "0.1.1" edition = "2021" -author = "Jeremy Wall " [lib] crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -wasm-web-component-macros = { path = "../macros" } +[dependencies.wasm-web-component-macros] +path = "../macros" [dependencies.wasm-bindgen-test] version = "0.3" @@ -26,7 +25,6 @@ version = "0.3" features = [ "CustomElementRegistry", "Document", - #"DocumentFragment", "KeyboardEvent", "Event", "EventTarget", @@ -35,7 +33,6 @@ features = [ "Text", "HtmlBaseElement", "HtmlElement", - "HtmlTemplateElement", "HtmlSlotElement", "Node", "ShadowRoot", @@ -44,3 +41,10 @@ features = [ "Window", "console" ] + +[features] +default = ["HtmlTemplateElement"] +HtmlTemplateElement = [ + "web-sys/HtmlTemplateElement", + "wasm-web-component-macros/HtmlTemplateElement", +] diff --git a/wasm-web-component/src/lib.rs b/wasm-web-component/src/lib.rs index e40500e..a314121 100644 --- a/wasm-web-component/src/lib.rs +++ b/wasm-web-component/src/lib.rs @@ -1,5 +1,7 @@ use js_sys::Function; use wasm_bindgen::{convert::IntoWasmAbi, JsValue}; +#[cfg(feature = "HtmlTemplateElement")] +use web_sys::HtmlTemplateElement; use web_sys::{window, Element, Event, HtmlElement, Window}; /// This attribute proc-macro will generate the following trait implementations @@ -82,10 +84,22 @@ pub trait WebComponentBinding: WebComponentDef { } } -/// 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. 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 /// element. pub struct WebComponentHandle { @@ -301,4 +315,32 @@ mod tests { assert_eq!(ThisElement::class_name(), "ThisElement"); assert_eq!(ThisElement::element_name(), "this-old-element"); } + + // 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 + } + } + + let body = window().unwrap().document().unwrap().body().unwrap(); + assert!(!body.last_child().unwrap().has_type::()); + MyTemplate::define_once(); + assert!(body.last_child().unwrap().has_type::()); + } }