mirror of
https://github.com/zaphar/wasm-web-components.git
synced 2025-07-21 19:40:30 -04:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e11072b52 | |||
aaea07a7b8 | |||
5ba459bcb6 | |||
2b00383712 | |||
a8d99c6284 | |||
f03d1641d9 |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasm-web-component-macros"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -33,14 +33,23 @@ fn expand_crate_ref(name: &str, path: Path) -> syn::Path {
|
||||
}
|
||||
}
|
||||
|
||||
struct AttributeConfig {
|
||||
class_name: Literal,
|
||||
element_name: Literal,
|
||||
observed_attributes: Literal,
|
||||
observed_events: Literal,
|
||||
base_class: Literal,
|
||||
}
|
||||
|
||||
fn get_class_and_element_names(
|
||||
args: Vec<NestedMeta>,
|
||||
struct_name: &Ident,
|
||||
) -> (Literal, Literal, Literal, Literal) {
|
||||
) -> AttributeConfig {
|
||||
let mut class_name = None;
|
||||
let mut element_name = None;
|
||||
let mut observed_attributes = None;
|
||||
let mut observed_events = None;
|
||||
let mut base_class = None;
|
||||
for arg in args {
|
||||
if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
|
||||
if nv.path.is_ident("class_name") {
|
||||
@ -59,6 +68,10 @@ fn get_class_and_element_names(
|
||||
if let Lit::Str(nm) = nv.lit {
|
||||
observed_events = Some(nm);
|
||||
}
|
||||
} else if nv.path.is_ident("base_class") {
|
||||
if let Lit::Str(nm) = nv.lit {
|
||||
base_class = Some(nm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,6 +87,7 @@ fn get_class_and_element_names(
|
||||
LitStr::new(&class_kebab, Span::call_site()).token()
|
||||
}
|
||||
};
|
||||
let base_class = base_class.unwrap_or_else(|| LitStr::new("HTMLElement", Span::call_site())).token();
|
||||
|
||||
let observed_attributes = observed_attributes
|
||||
.map(|n| n.token())
|
||||
@ -81,7 +95,13 @@ fn get_class_and_element_names(
|
||||
let observed_events = observed_events
|
||||
.map(|n| n.token())
|
||||
.unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token());
|
||||
(class_name, element_name, observed_attributes, observed_events)
|
||||
AttributeConfig {
|
||||
class_name,
|
||||
element_name,
|
||||
observed_attributes,
|
||||
observed_events,
|
||||
base_class,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_component_def(
|
||||
@ -104,13 +124,18 @@ fn expand_component_def(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jwall): Stateful elements?
|
||||
fn expand_wc_struct_trait_shim(
|
||||
struct_name: &Ident,
|
||||
once_name: &Ident,
|
||||
observed_attrs: Literal,
|
||||
observed_events: Literal,
|
||||
config: AttributeConfig,
|
||||
) -> syn::ItemImpl {
|
||||
let AttributeConfig {
|
||||
class_name: _,
|
||||
element_name: _,
|
||||
observed_attributes,
|
||||
observed_events,
|
||||
base_class,
|
||||
} = config;
|
||||
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! {
|
||||
@ -140,7 +165,7 @@ fn expand_wc_struct_trait_shim(
|
||||
return Err("Custom Element has already been defined".into());
|
||||
}
|
||||
let body = format!(
|
||||
"class {name} extends HTMLElement {{
|
||||
"class {name} extends {base_class} {{
|
||||
constructor() {{
|
||||
super();
|
||||
this._impl = impl();
|
||||
@ -181,7 +206,6 @@ fn expand_wc_struct_trait_shim(
|
||||
this._impl.attribute_changed_impl(this, name, oldValue, newValue);
|
||||
}}
|
||||
|
||||
// TODO(jwall): We need to provide a way to attach this event handler properly
|
||||
handleComponentEvent(evt) {{
|
||||
this._impl.handle_component_event_impl(this, evt);
|
||||
}}
|
||||
@ -191,8 +215,9 @@ var element = customElements.get(\"{element_name}\");
|
||||
return element;",
|
||||
name = Self::class_name(),
|
||||
element_name = Self::element_name(),
|
||||
observed_attributes = #observed_attrs,
|
||||
observed_attributes = #observed_attributes,
|
||||
observed_events = #observed_events,
|
||||
base_class = #base_class,
|
||||
);
|
||||
let fun = js_sys::Function::new_with_args("impl", &body);
|
||||
let f: Box<dyn FnMut() -> Self> = Box::new(|| {
|
||||
@ -224,6 +249,19 @@ fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc = "Attach an open shadowroot to our element."]
|
||||
pub fn attach_shadow(&self, element: &web_sys::HtmlElement, root: &str) {
|
||||
self.attach_shadow_with_mode(element, root, web_sys::ShadowRootMode::Open);
|
||||
}
|
||||
|
||||
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc = "Attach a shadowroot with the given mode to our element."]
|
||||
pub fn attach_shadow_with_mode(&self, element: &web_sys::HtmlElement, root: &str, mode: web_sys::ShadowRootMode) {
|
||||
let shadow_root = element.attach_shadow(&web_sys::ShadowRootInit::new(mode)).unwrap();
|
||||
shadow_root.set_inner_html(root);
|
||||
}
|
||||
|
||||
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn init_impl(&mut self, element: &web_sys::HtmlElement) {
|
||||
use #trait_path;
|
||||
@ -284,19 +322,16 @@ fn expand_binding(struct_name: &Ident) -> syn::ItemImpl {
|
||||
|
||||
fn expand_web_component_struct(
|
||||
item_struct: ItemStruct,
|
||||
class_name: Literal,
|
||||
element_name: Literal,
|
||||
observed_attributes: Literal,
|
||||
observed_events: Literal,
|
||||
config: AttributeConfig,
|
||||
) -> 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 component_def = expand_component_def(&struct_name, &class_name, &element_name);
|
||||
let component_def = expand_component_def(&struct_name, &config.class_name, &config.element_name);
|
||||
let non_wasm_impl =
|
||||
expand_wc_struct_trait_shim(&struct_name, &struct_once_name, observed_attributes, observed_events);
|
||||
expand_wc_struct_trait_shim(&struct_name, &struct_once_name, config);
|
||||
let wasm_shim = expand_wasm_shim(&struct_name);
|
||||
let binding_trait = expand_binding(&struct_name);
|
||||
let expanded = quote! {
|
||||
@ -354,15 +389,14 @@ fn expand_template_struct(item_struct: ItemStruct) -> TokenStream {
|
||||
/// Creates the necessary Rust and Javascript shims for a Web Component.
|
||||
#[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, observed_events) =
|
||||
let config =
|
||||
get_class_and_element_names(args, &item_struct.ident);
|
||||
|
||||
expand_web_component_struct(item_struct, class_name, element_name, observed_attributes, observed_events)
|
||||
expand_web_component_struct(item_struct, config)
|
||||
}
|
||||
|
||||
/// Creates the neccessary Rust and Javascript shims for rendering an HtmlTemplateElement
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasm-web-component"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
@ -19,18 +19,25 @@ use web_sys::{window, Element, Event, HtmlElement, Window};
|
||||
/// * `element_name = "class-name"` - A valid custom element name to use for the element. if not proviced derives it from the class name.
|
||||
/// * `observed_attrs = "['attr1', 'attr2']"` - A javascript array with a list of observed attributes for this compoment. Defaults to "[]".
|
||||
/// * `observed_events = "['click', 'change']"` - A javascript array with a list of observed event types for this compoment. Defaults to "[]".
|
||||
/// * `base_class = "HTMLInputElement"` - The HTMLElement base class this custom-element should
|
||||
/// inherit from. Defaults to "HTMLElement".
|
||||
///
|
||||
/// It will also create a `Self::define_once` method that will define the WebComponent exactly
|
||||
/// once.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```ignore
|
||||
/// use web_sys::*;
|
||||
/// use wasm_bindgen::*;
|
||||
/// use wasm_web_component::{web_component, WebComponent, WebComponentHandle, WebComponentDef, WebComponentBinding};
|
||||
///
|
||||
/// #[web_component(
|
||||
/// class_name = "MyElement",
|
||||
/// element_name = "my-element",
|
||||
/// observed_attrs = "['class']"
|
||||
/// observed_events = "['click']"
|
||||
/// observed_attrs = "['class']",
|
||||
/// observed_events = "['click']",
|
||||
/// base_class = "HTMLElement"
|
||||
/// )]
|
||||
/// pub struct MyElementImpl {}
|
||||
///
|
||||
@ -69,7 +76,7 @@ use web_sys::{window, Element, Event, HtmlElement, Window};
|
||||
/// element.append_child(&node).unwrap();
|
||||
/// }
|
||||
///
|
||||
/// fn handle_event(&self, element: &HtmlElement, event: &Event)) {
|
||||
/// fn handle_event(&self, element: &HtmlElement, event: &Event) {
|
||||
/// // handle this event
|
||||
/// }
|
||||
/// }
|
||||
@ -98,7 +105,10 @@ pub use wasm_web_component_macros::web_component;
|
||||
/// if the template has not been defined yet `None` will get returned.
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```rust
|
||||
/// ```ignore
|
||||
/// use wasm_web_component::*;
|
||||
/// use wasm_bindgen::*;
|
||||
/// # #[cfg(feature = "HtmlTemplateElement")]
|
||||
/// #[template_element]
|
||||
/// pub struct MyTemplate ();
|
||||
/// impl TemplateElementRender for MyTemplate {
|
||||
@ -436,6 +446,94 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_component_mut() {
|
||||
#[web_component(
|
||||
class_name = "MyElementMut",
|
||||
element_name = "my-element-mut",
|
||||
observed_attrs = "['class']",
|
||||
)]
|
||||
pub struct MyElementMutImpl {}
|
||||
|
||||
impl WebComponentBinding for MyElementMutImpl {
|
||||
fn connected_mut(&mut self, element: &HtmlElement) {
|
||||
let node = Text::new().unwrap();
|
||||
node.set_text_content(Some("Added a text node on connect".into()));
|
||||
element.append_child(&node).unwrap();
|
||||
}
|
||||
|
||||
fn disconnected_mut(&mut self, element: &HtmlElement) {
|
||||
let node = element.first_child().unwrap();
|
||||
element.remove_child(&node).unwrap();
|
||||
}
|
||||
|
||||
fn adopted_mut(&mut self, element: &HtmlElement) {
|
||||
let node = Text::new().unwrap();
|
||||
node.set_text_content(Some("Added a text node on adopt".into()));
|
||||
element.append_child(&node).unwrap();
|
||||
}
|
||||
|
||||
fn attribute_changed_mut(
|
||||
&mut self,
|
||||
element: &HtmlElement,
|
||||
name: JsValue,
|
||||
old_value: JsValue,
|
||||
new_value: JsValue,
|
||||
) {
|
||||
let node = element.first_child().unwrap();
|
||||
node.set_text_content(Some(&format!(
|
||||
"Setting {} from {} to {}",
|
||||
name.as_string().unwrap_or("None".to_owned()),
|
||||
old_value.as_string().unwrap_or("None".to_owned()),
|
||||
new_value.as_string().unwrap_or("None".to_owned()),
|
||||
)));
|
||||
element.append_child(&node).unwrap();
|
||||
}
|
||||
}
|
||||
let obj = MyElementMutImpl::define().expect("Failed to define web component");
|
||||
let fun = obj.element_constructor.dyn_ref::<Function>().unwrap();
|
||||
assert_eq!(fun.name(), MyElementMutImpl::class_name());
|
||||
let element = MyElementMutImpl::create();
|
||||
assert_eq!(
|
||||
element.tag_name().to_uppercase(),
|
||||
MyElementMutImpl::element_name().to_uppercase()
|
||||
);
|
||||
let document = window().unwrap().document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
// Test the connected callback
|
||||
body.append_child(&element).unwrap();
|
||||
assert_eq!(
|
||||
element.text_content().unwrap(),
|
||||
"Added a text node on connect"
|
||||
);
|
||||
|
||||
// Test the disconnected callback
|
||||
body.remove_child(&element).unwrap();
|
||||
assert_eq!(element.text_content().unwrap(), "");
|
||||
|
||||
body.append_child(&element).unwrap();
|
||||
element.set_attribute("class", "foo").unwrap();
|
||||
assert_eq!(
|
||||
element.text_content().unwrap(),
|
||||
"Setting class from None to foo"
|
||||
);
|
||||
|
||||
// NOTE(jwall): If we are running headless then this can fail sometimes.
|
||||
// We don't fail the test when that happens.
|
||||
if let Ok(Some(new_window)) = window().unwrap().open() {
|
||||
// Test the adopted callback
|
||||
// First we need a new window with a new document to perform the adoption with.
|
||||
new_window.document().unwrap().adopt_node(&element).unwrap();
|
||||
assert_eq!(
|
||||
element.text_content().unwrap(),
|
||||
"Added a text node on adopt"
|
||||
);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_component_no_element_name() {
|
||||
#[web_component(class_name = "AnElement")]
|
||||
|
Loading…
x
Reference in New Issue
Block a user