mirror of
https://github.com/zaphar/wasm-web-components.git
synced 2025-07-21 19:40:30 -04:00
Sketch out interface with shim implementation
This commit is contained in:
parent
4daaa03093
commit
d56f34cab9
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(),
|
||||
|
13
web-component/src/macros.rs
Normal file
13
web-component/src/macros.rs
Normal 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.
|
Loading…
x
Reference in New Issue
Block a user