diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 7ed1376..3d016a5 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,6 +11,7 @@ proc-macro = true [dependencies] quote = "1.0" proc-macro2 = "1.0" +proc-macro-crate = "1.2.1" [dependencies.syn] version = "1.0.101" diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b2cb91c..a3b42a4 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -13,17 +13,30 @@ // limitations under the License. use proc_macro::TokenStream; -use proc_macro2::Span; +use proc_macro2::{Literal, Span}; +use proc_macro_crate::{crate_name, FoundCrate}; use quote::quote; -use syn::{parse_macro_input, AttributeArgs, ItemStruct, Lit, LitStr, Meta, NestedMeta}; +use syn::{ + parse_macro_input, parse_quote, AttributeArgs, Ident, ItemStruct, Lit, LitStr, Meta, + NestedMeta, Path, +}; -#[proc_macro_attribute] -pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { - // TODO(jwall): Attrs for class name and element name - // Gather our attributes - let args = parse_macro_input!(attr as AttributeArgs); +fn expand_crate_ref(name: &str, path: Path) -> syn::Path { + let found_crate = crate_name(name).expect(&format!("{} is present in `Cargo.toml`", name)); + + match found_crate { + FoundCrate::Itself => parse_quote!( crate::#path ), + FoundCrate::Name(name) => { + let ident = Ident::new(&name, Span::call_site()); + parse_quote!( #ident::#path ) + } + } +} + +fn get_class_and_element_names(args: Vec) -> (Literal, Literal, Literal) { let mut class_name = None; let mut element_name = None; + let mut observed_attributes = None; for arg in args { if let NestedMeta::Meta(Meta::NameValue(nv)) = arg { if nv.path.is_ident("class_name") { @@ -34,32 +47,61 @@ pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { if let Lit::Str(nm) = nv.lit { element_name = Some(nm); } + } else if nv.path.is_ident("observed_attrs") { + if let Lit::Str(nm) = nv.lit { + observed_attributes = Some(nm); + } } } } - let element_name = element_name - .map(|n| n.token()) - .unwrap_or_else(|| LitStr::new("", Span::call_site()).token()); let class_name = class_name .map(|n| n.token()) .unwrap_or_else(|| LitStr::new("", Span::call_site()).token()); - let item_struct = parse_macro_input!(item as ItemStruct); - let struct_name = item_struct.ident.clone(); - let expanded = quote! { - #[wasm_bindgen] - #item_struct - impl #struct_name { - pub fn element_name() -> &'static str { + let element_name = element_name + .map(|n| n.token()) + .unwrap_or_else(|| LitStr::new("", Span::call_site()).token()); + let observed_attributes = observed_attributes + .map(|n| n.token()) + .unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token()); + (class_name, element_name, observed_attributes) +} + +fn expand_component_def( + struct_name: &Ident, + class_name: &Literal, + element_name: &Literal, +) -> syn::ItemImpl { + let trait_path = expand_crate_ref("web-component-rs", parse_quote!(WebComponentDef)); + parse_quote! { + impl #trait_path for #struct_name { + fn element_name() -> &'static str { #element_name } - pub fn class_name() -> &'static str { + fn class_name() -> &'static str { #class_name } - pub fn define() -> Result> { - use js_sys::Function; + } + } +} + +fn expand_struct_trait_shim(struct_name: &Ident, observed_attrs: Literal) -> syn::ItemImpl { + let trait_path = expand_crate_ref("web-component-rs", parse_quote!(WebComponentDef)); + let handle_path = expand_crate_ref("web-component-rs", parse_quote!(WebComponentHandle)); + parse_quote! { + impl #struct_name { + pub fn element_name() -> &'static str { + ::element_name() + } + + pub fn class_name() -> &'static str { + ::class_name() + } + + pub fn define() -> std::result::Result<#handle_path<#struct_name>, JsValue> { + use wasm_bindgen::JsCast; use web_sys::{window, Element, HtmlElement}; let registry = web_sys::window().unwrap().custom_elements(); let maybe_element = registry.get(Self::element_name()); @@ -75,15 +117,16 @@ pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { connectedCallback() {{ this._impl.connected_impl(this); + console.log(this.textContent); }} disconnectedCallback() {{ this._impl.disconnected_impl(this); + console.log(this.textContent); }} static get observedAttributes() {{ - console.log('observed attributes: ', attrs); - return attrs; + return {observed_attributes}; }} adoptedCallback() {{ @@ -100,18 +143,18 @@ var element = customElements.get(\"{element_name}\"); return element;", name = Self::class_name(), element_name = Self::element_name(), + observed_attributes = #observed_attrs, ); - let fun = Function::new_with_args("impl, attrs", &body); + let fun = Function::new_with_args("impl", &body); let f: Box Self> = Box::new(|| { let obj = Self::new(); obj }); let constructor_handle = Closure::wrap(f); let element = fun - .call2( + .call1( &window().unwrap(), constructor_handle.as_ref().unchecked_ref::(), - &Self::observed_attributes(), )? .dyn_into()?; Ok(WebComponentHandle { @@ -120,7 +163,102 @@ return element;", }) } } + } +} + +fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl { + let trait_path = expand_crate_ref("web-component-rs", parse_quote!(WebComponentBinding)); + parse_quote! { + #[wasm_bindgen::prelude::wasm_bindgen] + impl #struct_name { + #[wasm_bindgen::prelude::wasm_bindgen(constructor)] + pub fn new() -> Self { + Self::default() + } + + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn create() -> web_sys::Element { + window() + .unwrap() + .document() + .unwrap() + .create_element(Self::element_name()) + .unwrap() + } + + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn connected_impl(&self, element: &web_sys::HtmlElement) { + use #trait_path; + self.connected(element); + } + + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn disconnected_impl(&self, element: &web_sys::HtmlElement) { + use #trait_path; + self.disconnected(element); + } + + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn adopted_impl(&self, element: &web_sys::HtmlElement) { + use #trait_path; + self.adopted(element); + } + + + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn attribute_changed_impl( + &self, + element: &web_sys::HtmlElement, + name: wasm_bindgen::JsValue, + old_value: wasm_bindgen::JsValue, + new_value: wasm_bindgen::JsValue, + ) { + use #trait_path; + self.attribute_changed(element, name, old_value, new_value); + } + } + } +} + +fn expand_binding(struct_name: &Ident) -> syn::ItemImpl { + let trait_path = expand_crate_ref("web-component-rs", parse_quote!(WebComponent)); + parse_quote!( + impl #trait_path for #struct_name {} + ) +} + +fn expand_struct( + item_struct: ItemStruct, + class_name: Literal, + element_name: Literal, + observed_attributes: Literal, +) -> TokenStream { + let struct_name = item_struct.ident.clone(); + let component_def = expand_component_def(&struct_name, &class_name, &element_name); + let non_wasm_impl = expand_struct_trait_shim(&struct_name, observed_attributes); + let wasm_shim = expand_wasm_shim(&struct_name); + let binding_trait = expand_binding(&struct_name); + let expanded = quote! { + #[wasm_bindgen::prelude::wasm_bindgen] + #[derive(Default, Debug)] + #item_struct + #component_def + #non_wasm_impl + #binding_trait + #wasm_shim }; TokenStream::from(expanded) } + +#[proc_macro_attribute] +pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { + // TODO(jwall): Attrs for class name and element name + // Gather our attributes + let args = parse_macro_input!(attr as AttributeArgs); + let item_struct = parse_macro_input!(item as ItemStruct); + + let (class_name, element_name, observed_attributes) = get_class_and_element_names(args); + + expand_struct(item_struct, class_name, element_name, observed_attributes) +} diff --git a/web-component/src/lib.rs b/web-component/src/lib.rs index 810bbbf..621e87d 100644 --- a/web-component/src/lib.rs +++ b/web-component/src/lib.rs @@ -1,15 +1,55 @@ use js_sys::Function; -use wasm_bindgen::{convert::IntoWasmAbi, prelude::*, JsCast, JsValue}; +use wasm_bindgen::{convert::IntoWasmAbi, prelude::Closure, JsValue}; use web_sys::{window, Element, HtmlElement}; -use web_component_derive::web_component; +pub mod macros; -type Result = std::result::Result; +pub trait WebComponentDef: IntoWasmAbi + Default { + fn new() -> Self { + Self::default() + } + + 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 trait CustomElementImpl: IntoWasmAbi {} - -pub struct WebComponentHandle { +pub struct WebComponentHandle { pub impl_handle: Closure T>, pub element_constructor: Function, } @@ -17,52 +57,42 @@ pub struct WebComponentHandle { #[cfg(test)] mod tests { use super::*; - use wasm_bindgen::JsCast; + use wasm_bindgen::prelude::wasm_bindgen; + use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_test::wasm_bindgen_test; use web_sys::Text; + use web_sys::{window, HtmlElement}; + + use web_component_derive::web_component; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - #[web_component(class_name = "MyElement", element_name = "my-element")] - #[derive(Default, Debug)] + #[web_component( + class_name = "MyElement", + element_name = "my-element", + observed_attrs = "['class']" + )] pub struct MyElementImpl {} - impl CustomElementImpl for MyElementImpl {} - - #[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) { + 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_with_val("element: ".to_owned(), element); + log(format!( + "element contents: {}", + &element.text_content().unwrap() + )); } - #[wasm_bindgen] - pub fn disconnected_impl(&self, element: &HtmlElement) { + fn disconnected(&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) { + 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())); @@ -70,14 +100,7 @@ mod tests { 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( + fn attribute_changed( &self, element: &HtmlElement, name: JsValue, @@ -121,17 +144,17 @@ mod tests { let body = document.body().unwrap(); // Test the connected callback - let node = body.append_child(element.as_ref()).unwrap(); + body.append_child(&element).unwrap(); assert_eq!( element.text_content().unwrap(), "Added a text node on connect" ); // Test the disconnected callback - body.remove_child(&node).unwrap(); + body.remove_child(&element).unwrap(); assert_eq!(element.text_content().unwrap(), ""); - body.append_child(element.as_ref()).unwrap(); + body.append_child(&element).unwrap(); element.set_attribute("class", "foo").unwrap(); assert_eq!( element.text_content().unwrap(), diff --git a/web-component/src/macros.rs b/web-component/src/macros.rs new file mode 100644 index 0000000..8068d9e --- /dev/null +++ b/web-component/src/macros.rs @@ -0,0 +1,13 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.