Sketch out interface with shim implementation

This commit is contained in:
Jeremy Wall 2022-10-05 20:31:29 -04:00
parent 4daaa03093
commit d56f34cab9
4 changed files with 246 additions and 71 deletions

View File

@ -11,6 +11,7 @@ proc-macro = true
[dependencies] [dependencies]
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
proc-macro-crate = "1.2.1"
[dependencies.syn] [dependencies.syn]
version = "1.0.101" version = "1.0.101"

View File

@ -13,17 +13,30 @@
// limitations under the License. // limitations under the License.
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Span; use proc_macro2::{Literal, Span};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote; 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] fn expand_crate_ref(name: &str, path: Path) -> syn::Path {
pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream { let found_crate = crate_name(name).expect(&format!("{} is present in `Cargo.toml`", name));
// TODO(jwall): Attrs for class name and element name
// Gather our attributes match found_crate {
let args = parse_macro_input!(attr as AttributeArgs); 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<NestedMeta>) -> (Literal, Literal, Literal) {
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;
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") {
@ -34,32 +47,61 @@ pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
if let Lit::Str(nm) = nv.lit { if let Lit::Str(nm) = nv.lit {
element_name = Some(nm); 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 let class_name = class_name
.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());
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 { let element_name = element_name
pub fn element_name() -> &'static str { .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 #element_name
} }
pub fn class_name() -> &'static str { fn class_name() -> &'static str {
#class_name #class_name
} }
pub fn define() -> Result<WebComponentHandle<#struct_name>> { }
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 {
<Self as #trait_path>::element_name()
}
pub fn class_name() -> &'static str {
<Self as #trait_path>::class_name()
}
pub fn define() -> std::result::Result<#handle_path<#struct_name>, JsValue> {
use wasm_bindgen::JsCast;
use web_sys::{window, Element, HtmlElement}; use web_sys::{window, 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());
@ -75,15 +117,16 @@ pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
connectedCallback() {{ connectedCallback() {{
this._impl.connected_impl(this); this._impl.connected_impl(this);
console.log(this.textContent);
}} }}
disconnectedCallback() {{ disconnectedCallback() {{
this._impl.disconnected_impl(this); this._impl.disconnected_impl(this);
console.log(this.textContent);
}} }}
static get observedAttributes() {{ static get observedAttributes() {{
console.log('observed attributes: ', attrs); return {observed_attributes};
return attrs;
}} }}
adoptedCallback() {{ adoptedCallback() {{
@ -100,18 +143,18 @@ 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,
); );
let fun = Function::new_with_args("impl, attrs", &body); let fun = 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 = Closure::wrap(f); let constructor_handle = Closure::wrap(f);
let element = fun let element = fun
.call2( .call1(
&window().unwrap(), &window().unwrap(),
constructor_handle.as_ref().unchecked_ref::<Function>(), constructor_handle.as_ref().unchecked_ref::<Function>(),
&Self::observed_attributes(),
)? )?
.dyn_into()?; .dyn_into()?;
Ok(WebComponentHandle { 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) 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)
}

View File

@ -1,15 +1,55 @@
use js_sys::Function; 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_sys::{window, Element, HtmlElement};
use web_component_derive::web_component; pub mod macros;
type Result<T> = std::result::Result<T, JsValue>; 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. // 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<T> {
pub struct WebComponentHandle<T: CustomElementImpl> {
pub impl_handle: Closure<dyn FnMut() -> T>, pub impl_handle: Closure<dyn FnMut() -> T>,
pub element_constructor: Function, pub element_constructor: Function,
} }
@ -17,52 +57,42 @@ pub struct WebComponentHandle<T: CustomElementImpl> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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 wasm_bindgen_test::wasm_bindgen_test;
use web_sys::Text; 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); wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[web_component(class_name = "MyElement", element_name = "my-element")] #[web_component(
#[derive(Default, Debug)] class_name = "MyElement",
element_name = "my-element",
observed_attrs = "['class']"
)]
pub struct MyElementImpl {} pub struct MyElementImpl {}
impl CustomElementImpl for MyElementImpl {} impl WebComponentBinding for MyElementImpl {
fn connected(&self, element: &HtmlElement) {
#[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()); log("Firing connected call back".to_owned());
let node = Text::new().unwrap(); let node = Text::new().unwrap();
node.set_text_content(Some("Added a text node on connect".into())); node.set_text_content(Some("Added a text node on connect".into()));
element.append_child(&node).unwrap(); element.append_child(&node).unwrap();
log_with_val("element: ".to_owned(), element); log(format!(
"element contents: {}",
&element.text_content().unwrap()
));
} }
#[wasm_bindgen] fn disconnected(&self, element: &HtmlElement) {
pub fn disconnected_impl(&self, element: &HtmlElement) {
log("Firing discconnected call back".to_owned()); log("Firing discconnected call back".to_owned());
let node = element.first_child().unwrap(); let node = element.first_child().unwrap();
element.remove_child(&node).unwrap(); element.remove_child(&node).unwrap();
} }
#[wasm_bindgen] fn adopted(&self, element: &HtmlElement) {
pub fn adopted_impl(&self, element: &HtmlElement) {
log("Firing adopted call back".to_owned()); log("Firing adopted call back".to_owned());
let node = Text::new().unwrap(); let node = Text::new().unwrap();
node.set_text_content(Some("Added a text node on adopt".into())); 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); log_with_val("element: ".to_owned(), element);
} }
pub fn observed_attributes() -> js_sys::Array { fn attribute_changed(
let attrs = js_sys::Array::new();
attrs.push(&JsValue::from_str("class"));
attrs
}
#[wasm_bindgen]
pub fn attribute_changed_impl(
&self, &self,
element: &HtmlElement, element: &HtmlElement,
name: JsValue, name: JsValue,
@ -121,17 +144,17 @@ mod tests {
let body = document.body().unwrap(); let body = document.body().unwrap();
// Test the connected callback // Test the connected callback
let node = body.append_child(element.as_ref()).unwrap(); body.append_child(&element).unwrap();
assert_eq!( assert_eq!(
element.text_content().unwrap(), element.text_content().unwrap(),
"Added a text node on connect" "Added a text node on connect"
); );
// Test the disconnected callback // Test the disconnected callback
body.remove_child(&node).unwrap(); body.remove_child(&element).unwrap();
assert_eq!(element.text_content().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(); element.set_attribute("class", "foo").unwrap();
assert_eq!( assert_eq!(
element.text_content().unwrap(), element.text_content().unwrap(),

View File

@ -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.