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]
quote = "1.0"
proc-macro2 = "1.0"
proc-macro-crate = "1.2.1"
[dependencies.syn]
version = "1.0.101"

View File

@ -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<NestedMeta>) -> (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<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};
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<dyn FnMut() -> 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::<Function>(),
&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)
}

View File

@ -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<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.
pub trait CustomElementImpl: IntoWasmAbi {}
pub struct WebComponentHandle<T: CustomElementImpl> {
pub struct WebComponentHandle<T> {
pub impl_handle: Closure<dyn FnMut() -> T>,
pub element_constructor: Function,
}
@ -17,52 +57,42 @@ pub struct WebComponentHandle<T: CustomElementImpl> {
#[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(),

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.