mirror of
https://github.com/zaphar/wasm-web-components.git
synced 2025-07-22 19:50:07 -04:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e11072b52 | |||
aaea07a7b8 | |||
5ba459bcb6 | |||
2b00383712 | |||
a8d99c6284 | |||
f03d1641d9 | |||
a63670b7d3 | |||
7b589373ed | |||
8ce79d34a5 | |||
bb60556f29 | |||
f41a9d3100 | |||
76baa1871e | |||
8956f485a8 | |||
c39293636d | |||
7f25fae4dc | |||
2f7c3793c5 | |||
e71a28b4e3 | |||
f5791b9355 | |||
9dbdccef78 | |||
55d743f01c |
5
Makefile
Normal file
5
Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
cargo build;
|
||||||
|
|
||||||
|
test:
|
||||||
|
cd wasm-web-component; wasm-pack test --headless --firefox
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wasm-web-component-macros"
|
name = "wasm-web-component-macros"
|
||||||
version = "0.1.1"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@ -17,3 +17,6 @@ str_inflector = "0.12.0"
|
|||||||
[dependencies.syn]
|
[dependencies.syn]
|
||||||
version = "1.0.101"
|
version = "1.0.101"
|
||||||
features = ["full"]
|
features = ["full"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
HtmlTemplateElement = []
|
||||||
|
@ -33,13 +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(
|
fn get_class_and_element_names(
|
||||||
args: Vec<NestedMeta>,
|
args: Vec<NestedMeta>,
|
||||||
struct_name: &Ident,
|
struct_name: &Ident,
|
||||||
) -> (Literal, Literal, Literal) {
|
) -> AttributeConfig {
|
||||||
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;
|
let mut observed_attributes = None;
|
||||||
|
let mut observed_events = None;
|
||||||
|
let mut base_class = 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") {
|
||||||
@ -54,6 +64,14 @@ fn get_class_and_element_names(
|
|||||||
if let Lit::Str(nm) = nv.lit {
|
if let Lit::Str(nm) = nv.lit {
|
||||||
observed_attributes = Some(nm);
|
observed_attributes = Some(nm);
|
||||||
}
|
}
|
||||||
|
} else if nv.path.is_ident("observed_events") {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,15 +83,25 @@ fn get_class_and_element_names(
|
|||||||
let element_name = match element_name.map(|n| n.token()) {
|
let element_name = match element_name.map(|n| n.token()) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => {
|
None => {
|
||||||
let class_kebab = class_name.to_string().to_kebab_case();
|
let class_kebab = class_name.to_string().to_kebab_case().to_lowercase();
|
||||||
LitStr::new(&class_kebab, Span::call_site()).token()
|
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
|
let observed_attributes = observed_attributes
|
||||||
.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());
|
||||||
(class_name, element_name, observed_attributes)
|
let observed_events = observed_events
|
||||||
|
.map(|n| n.token())
|
||||||
|
.unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token());
|
||||||
|
AttributeConfig {
|
||||||
|
class_name,
|
||||||
|
element_name,
|
||||||
|
observed_attributes,
|
||||||
|
observed_events,
|
||||||
|
base_class,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_component_def(
|
fn expand_component_def(
|
||||||
@ -96,7 +124,18 @@ fn expand_component_def(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_attrs: Literal) -> syn::ItemImpl {
|
fn expand_wc_struct_trait_shim(
|
||||||
|
struct_name: &Ident,
|
||||||
|
once_name: &Ident,
|
||||||
|
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 trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef));
|
||||||
let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle));
|
let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle));
|
||||||
parse_quote! {
|
parse_quote! {
|
||||||
@ -115,21 +154,32 @@ fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_att
|
|||||||
let _ = Self::define();
|
let _ = Self::define();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc = "Defines this web component element if not defined already otherwise returns an error."]
|
#[doc = "Defines this web component element if not defined already otherwise returns an error."]
|
||||||
pub fn define() -> std::result::Result<#handle_path, JsValue> {
|
pub fn define() -> std::result::Result<#handle_path, ::wasm_bindgen::JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
use ::wasm_bindgen::JsCast;
|
||||||
use web_sys::{window, Element, HtmlElement};
|
use web_sys::{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());
|
||||||
if maybe_element.is_truthy() {
|
if maybe_element.is_truthy() {
|
||||||
return Err("Custom Element has already been defined".into());
|
return Err("Custom Element has already been defined".into());
|
||||||
}
|
}
|
||||||
let body = format!(
|
let body = format!(
|
||||||
"class {name} extends HTMLElement {{
|
"class {name} extends {base_class} {{
|
||||||
constructor() {{
|
constructor() {{
|
||||||
super();
|
super();
|
||||||
this._impl = impl();
|
this._impl = impl();
|
||||||
|
this._impl.init_impl(this);
|
||||||
|
var self = this;
|
||||||
|
if (self.shadowRoot) {{
|
||||||
|
for (const t of this.observedEvents()) {{
|
||||||
|
self.shadowRoot.addEventListener(t, function(evt) {{ self.handleComponentEvent(evt); }} );
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
for (const t of self.observedEvents()) {{
|
||||||
|
self.addEventListener(t, function(evt) {{ self.handleComponentEvent(evt); }} );
|
||||||
|
}}
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
connectedCallback() {{
|
connectedCallback() {{
|
||||||
@ -144,11 +194,15 @@ fn expand_struct_trait_shim(struct_name: &Ident, once_name: &Ident, observed_att
|
|||||||
return {observed_attributes};
|
return {observed_attributes};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
observedEvents() {{
|
||||||
|
return {observed_events};
|
||||||
|
}}
|
||||||
|
|
||||||
adoptedCallback() {{
|
adoptedCallback() {{
|
||||||
this._impl.adopted_impl(this);
|
this._impl.adopted_impl(this);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {{
|
attributeChangedCallback(name, oldValue, newValue) {{
|
||||||
this._impl.attribute_changed_impl(this, name, oldValue, newValue);
|
this._impl.attribute_changed_impl(this, name, oldValue, newValue);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@ -161,17 +215,19 @@ 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,
|
observed_attributes = #observed_attributes,
|
||||||
|
observed_events = #observed_events,
|
||||||
|
base_class = #base_class,
|
||||||
);
|
);
|
||||||
let fun = js_sys::Function::new_with_args("impl", &body);
|
let fun = js_sys::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 = wasm_bindgen::prelude::Closure::wrap(f).into_js_value().unchecked_into::<js_sys::Function>();
|
let constructor_handle = ::wasm_bindgen::prelude::Closure::wrap(f).into_js_value().unchecked_into::<js_sys::Function>();
|
||||||
let element = fun
|
let element = fun
|
||||||
.call1(
|
.call1(
|
||||||
&window().unwrap(),
|
&web_sys::window().unwrap(),
|
||||||
constructor_handle.as_ref(),
|
constructor_handle.as_ref(),
|
||||||
)?
|
)?
|
||||||
.dyn_into()?;
|
.dyn_into()?;
|
||||||
@ -186,47 +242,72 @@ return element;",
|
|||||||
fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl {
|
fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl {
|
||||||
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentBinding));
|
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentBinding));
|
||||||
parse_quote! {
|
parse_quote! {
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
impl #struct_name {
|
impl #struct_name {
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen(constructor)]
|
#[::wasm_bindgen::prelude::wasm_bindgen(constructor)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn connected_impl(&self, element: &web_sys::HtmlElement) {
|
#[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;
|
||||||
|
self.init(element);
|
||||||
|
self.init_mut(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn connected_impl(&mut self, element: &web_sys::HtmlElement) {
|
||||||
use #trait_path;
|
use #trait_path;
|
||||||
self.connected(element);
|
self.connected(element);
|
||||||
|
self.connected_mut(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn disconnected_impl(&self, element: &web_sys::HtmlElement) {
|
pub fn disconnected_impl(&mut self, element: &web_sys::HtmlElement) {
|
||||||
use #trait_path;
|
use #trait_path;
|
||||||
self.disconnected(element);
|
self.disconnected(element);
|
||||||
|
self.disconnected_mut(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn adopted_impl(&self, element: &web_sys::HtmlElement) {
|
pub fn adopted_impl(&mut self, element: &web_sys::HtmlElement) {
|
||||||
use #trait_path;
|
use #trait_path;
|
||||||
self.adopted(element);
|
self.adopted(element);
|
||||||
|
self.adopted_mut(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn attribute_changed_impl(
|
pub fn attribute_changed_impl(
|
||||||
&self,
|
&mut self,
|
||||||
element: &web_sys::HtmlElement,
|
element: &web_sys::HtmlElement,
|
||||||
name: wasm_bindgen::JsValue,
|
name: ::wasm_bindgen::JsValue,
|
||||||
old_value: wasm_bindgen::JsValue,
|
old_value: ::wasm_bindgen::JsValue,
|
||||||
new_value: wasm_bindgen::JsValue,
|
new_value: ::wasm_bindgen::JsValue,
|
||||||
) {
|
) {
|
||||||
use #trait_path;
|
use #trait_path;
|
||||||
self.attribute_changed(element, name, old_value, new_value);
|
self.attribute_changed(element, name.clone(), old_value.clone(), new_value.clone());
|
||||||
|
self.attribute_changed_mut(element, name, old_value, new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_component_event_impl(&self, element: &web_sys::HtmlElement, event: &web_sys::Event) {
|
pub fn handle_component_event_impl(&mut self, element: &web_sys::HtmlElement, event: &web_sys::Event) {
|
||||||
use #trait_path;
|
use #trait_path;
|
||||||
self.handle_event(element, event);
|
self.handle_event(element, event);
|
||||||
|
self.handle_event_mut(element, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,22 +320,24 @@ fn expand_binding(struct_name: &Ident) -> syn::ItemImpl {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_struct(
|
fn expand_web_component_struct(
|
||||||
item_struct: ItemStruct,
|
item_struct: ItemStruct,
|
||||||
class_name: Literal,
|
config: AttributeConfig,
|
||||||
element_name: Literal,
|
|
||||||
observed_attributes: Literal,
|
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
let struct_name = item_struct.ident.clone();
|
let struct_name = item_struct.ident.clone();
|
||||||
let struct_once_name = Ident::new(&(struct_name.to_string() + "Once"), Span::call_site());
|
let struct_once_name = Ident::new(
|
||||||
let component_def = expand_component_def(&struct_name, &class_name, &element_name);
|
&(struct_name.to_string().to_snake_case().to_uppercase() + "_ONCE"),
|
||||||
let non_wasm_impl = expand_struct_trait_shim(&struct_name, &struct_once_name, observed_attributes);
|
Span::call_site(),
|
||||||
|
);
|
||||||
|
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, config);
|
||||||
let wasm_shim = expand_wasm_shim(&struct_name);
|
let wasm_shim = expand_wasm_shim(&struct_name);
|
||||||
let binding_trait = expand_binding(&struct_name);
|
let binding_trait = expand_binding(&struct_name);
|
||||||
let expanded = quote! {
|
let expanded = quote! {
|
||||||
use std::sync::Once;
|
#[allow(non_snake_case)]
|
||||||
static #struct_once_name: Once = Once::new();
|
static #struct_once_name: std::sync::Once = std::sync::Once::new();
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[::wasm_bindgen::prelude::wasm_bindgen]
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
#item_struct
|
#item_struct
|
||||||
#component_def
|
#component_def
|
||||||
@ -266,16 +349,60 @@ fn expand_struct(
|
|||||||
TokenStream::from(expanded)
|
TokenStream::from(expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
fn expand_template_struct(item_struct: ItemStruct) -> 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 trait_path = expand_crate_ref("wasm-web-component", parse_quote!(TemplateElement));
|
||||||
|
let expanded = quote! {
|
||||||
|
use web_sys::Node;
|
||||||
|
static #struct_once_name: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||||
|
#item_struct
|
||||||
|
impl #trait_path for #struct_name {}
|
||||||
|
impl #struct_name {
|
||||||
|
#[doc = "Defines this HtmlTemplateElement and adds it to the document exactly once. Subsequent calls are noops. Returns the the template element id it exists on the template element."]
|
||||||
|
pub fn define_once() -> Option<&'static Option<String>> {
|
||||||
|
#struct_once_name.get_or_init(|| {
|
||||||
|
let template_element = Self::render();
|
||||||
|
let id: Option<String> = template_element.get_attribute("id");
|
||||||
|
let body = web_sys::window().expect("Failed to get window")
|
||||||
|
.document().expect("Failed to get window document").
|
||||||
|
body().expect("Failed to get document body");
|
||||||
|
body.append_child(template_element.as_ref()).expect("Failed to add template element to document");
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
return #struct_once_name.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = "Returns the the template element id it exists. None if the element has not been defined yet. Some(&None) if the element has no id. Some(&Some(id)) if the element has an id."]
|
||||||
|
pub fn get_id() -> Option<&'static Option<String>> {
|
||||||
|
return #struct_once_name.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates the necessary Rust and Javascript shims for a Web Component.
|
/// Creates the necessary Rust and Javascript shims for a Web Component.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
// TODO(jwall): Attrs for class name and element name
|
|
||||||
// Gather our attributes
|
// Gather our attributes
|
||||||
let args = parse_macro_input!(attr as AttributeArgs);
|
let args = parse_macro_input!(attr as AttributeArgs);
|
||||||
let item_struct = parse_macro_input!(item as ItemStruct);
|
let item_struct = parse_macro_input!(item as ItemStruct);
|
||||||
|
|
||||||
let (class_name, element_name, observed_attributes) =
|
let config =
|
||||||
get_class_and_element_names(args, &item_struct.ident);
|
get_class_and_element_names(args, &item_struct.ident);
|
||||||
|
|
||||||
expand_struct(item_struct, class_name, element_name, observed_attributes)
|
expand_web_component_struct(item_struct, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the neccessary Rust and Javascript shims for rendering an HtmlTemplateElement
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn template_element(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let item_struct = parse_macro_input!(item as ItemStruct);
|
||||||
|
expand_template_struct(item_struct)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wasm-web-component"
|
name = "wasm-web-component"
|
||||||
version = "0.1.1"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
author = "Jeremy Wall <jeremy@marzhillstudios.com>"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies.wasm-web-component-macros]
|
||||||
wasm-web-component-macros = { path = "../macros" }
|
path = "../macros"
|
||||||
|
|
||||||
[dependencies.wasm-bindgen-test]
|
[dependencies.wasm-bindgen-test]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
|
||||||
[dependencies.wasm-bindgen]
|
[dependencies.wasm-bindgen]
|
||||||
version = "= 0.2.81"
|
version = "~0.2"
|
||||||
|
|
||||||
[dependencies.js-sys]
|
[dependencies.js-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
@ -25,9 +24,8 @@ version = "0.3"
|
|||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
"CustomElementRegistry",
|
"CustomElementRegistry",
|
||||||
|
"CustomEvent",
|
||||||
"Document",
|
"Document",
|
||||||
#"DocumentFragment",
|
|
||||||
"KeyboardEvent",
|
|
||||||
"Event",
|
"Event",
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
"Element",
|
"Element",
|
||||||
@ -35,7 +33,6 @@ features = [
|
|||||||
"Text",
|
"Text",
|
||||||
"HtmlBaseElement",
|
"HtmlBaseElement",
|
||||||
"HtmlElement",
|
"HtmlElement",
|
||||||
"HtmlTemplateElement",
|
|
||||||
"HtmlSlotElement",
|
"HtmlSlotElement",
|
||||||
"Node",
|
"Node",
|
||||||
"ShadowRoot",
|
"ShadowRoot",
|
||||||
@ -44,3 +41,10 @@ features = [
|
|||||||
"Window",
|
"Window",
|
||||||
"console"
|
"console"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["HtmlTemplateElement"]
|
||||||
|
HtmlTemplateElement = [
|
||||||
|
"web-sys/HtmlTemplateElement",
|
||||||
|
"wasm-web-component-macros/HtmlTemplateElement",
|
||||||
|
]
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use js_sys::Function;
|
use js_sys::Function;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::{convert::IntoWasmAbi, JsValue};
|
use wasm_bindgen::{convert::IntoWasmAbi, JsValue};
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
use web_sys::HtmlTemplateElement;
|
||||||
use web_sys::{window, Element, Event, HtmlElement, Window};
|
use web_sys::{window, Element, Event, HtmlElement, Window};
|
||||||
|
|
||||||
/// This attribute proc-macro will generate the following trait implementations
|
/// This attribute proc-macro will generate the following trait implementations
|
||||||
@ -14,11 +17,121 @@ use web_sys::{window, Element, Event, HtmlElement, Window};
|
|||||||
/// It supports three optional attributes `name = value` parameters.
|
/// It supports three optional attributes `name = value` parameters.
|
||||||
/// * `class_name = "ClassName"` - The class name to use for the javascript shim. If not provided uses the structs name instead.
|
/// * `class_name = "ClassName"` - The class name to use for the javascript shim. If not provided uses the structs name instead.
|
||||||
/// * `element_name = "class-name"` - A valid custom element name to use for the element. if not proviced derives it from the class name.
|
/// * `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_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
|
||||||
|
///
|
||||||
|
/// ```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']",
|
||||||
|
/// base_class = "HTMLElement"
|
||||||
|
/// )]
|
||||||
|
/// pub struct MyElementImpl {}
|
||||||
|
///
|
||||||
|
/// impl WebComponentBinding for MyElementImpl {
|
||||||
|
/// fn connected(&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(&self, element: &HtmlElement) {
|
||||||
|
/// let node = element.first_child().unwrap();
|
||||||
|
/// element.remove_child(&node).unwrap();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn adopted(&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(
|
||||||
|
/// &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();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn handle_event(&self, element: &HtmlElement, event: &Event) {
|
||||||
|
/// // handle this event
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// pub fn define_me() {
|
||||||
|
/// MyElementImpl::define_once();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
/// Reference [MDN Web Components Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
|
/// Reference [MDN Web Components Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
|
||||||
pub use wasm_web_component_macros::web_component;
|
pub use wasm_web_component_macros::web_component;
|
||||||
|
|
||||||
|
/// This attribute proc-macro will generate the following trait implementation
|
||||||
|
/// [TemplateElement](trait@TemplateElement)
|
||||||
|
///
|
||||||
|
/// It will also generate a wasm_bindgen compatible impl block for your struct. It expects
|
||||||
|
/// you to implement [TemplateElementRender](trait@TemplateElementRender) trait in order to
|
||||||
|
/// allow it to implement the methods using methods from that trait.
|
||||||
|
///
|
||||||
|
/// You can define the template element exactly once by calling the `Self::define_once` method.
|
||||||
|
/// Subsequent calls to that method will be a noop. It returns one of the following values:
|
||||||
|
/// * `Some(None)` If the template doesn't have an id.
|
||||||
|
/// * `Some(Some(id))` If the template has an id.
|
||||||
|
/// * `None` Should never get returned.
|
||||||
|
///
|
||||||
|
/// A `get_id` method will also get defined for you that returns the same values with the difference that
|
||||||
|
/// if the template has not been defined yet `None` will get returned.
|
||||||
|
///
|
||||||
|
/// ## Example usage
|
||||||
|
/// ```ignore
|
||||||
|
/// use wasm_web_component::*;
|
||||||
|
/// use wasm_bindgen::*;
|
||||||
|
/// # #[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
/// #[template_element]
|
||||||
|
/// pub struct MyTemplate ();
|
||||||
|
/// impl TemplateElementRender for MyTemplate {
|
||||||
|
/// fn render() -> HtmlTemplateElement {
|
||||||
|
/// let val: JsValue = window()
|
||||||
|
/// .unwrap()
|
||||||
|
/// .document()
|
||||||
|
/// .unwrap()
|
||||||
|
/// .create_element("template")
|
||||||
|
/// .unwrap()
|
||||||
|
/// .into();
|
||||||
|
/// let el: HtmlTemplateElement = val.into();
|
||||||
|
/// el.set_attribute("id", "template-id").unwrap();
|
||||||
|
/// return el;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// pub fn define_it() {
|
||||||
|
/// let id: Option<&'static Option<String>> = MyTemplate::define_once();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub use wasm_web_component_macros::template_element;
|
||||||
|
|
||||||
/// Helper trait for Rust Web Components. This is autogenerated
|
/// Helper trait for Rust Web Components. This is autogenerated
|
||||||
/// by the [`#[web_component]`](web_component) attribute.
|
/// by the [`#[web_component]`](web_component) attribute.
|
||||||
pub trait WebComponentDef: IntoWasmAbi + Default {
|
pub trait WebComponentDef: IntoWasmAbi + Default {
|
||||||
@ -27,15 +140,20 @@ pub trait WebComponentDef: IntoWasmAbi + Default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create() -> Element {
|
fn create() -> Element {
|
||||||
Self::create_in_window(window().unwrap())
|
Self::create_in_window(window().expect("Failed to get window"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_in_window(window: Window) -> Element {
|
fn create_in_window(window: Window) -> Element {
|
||||||
window
|
window
|
||||||
.document()
|
.document()
|
||||||
.unwrap()
|
.expect("Failed to get document")
|
||||||
.create_element(Self::element_name())
|
.create_element(Self::element_name())
|
||||||
.unwrap()
|
.expect("Failed to create element")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a custom event
|
||||||
|
fn custom_event(event_type: &str) -> web_sys::Event {
|
||||||
|
web_sys::CustomEvent::new(event_type).unwrap().dyn_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn element_name() -> &'static str;
|
fn element_name() -> &'static str;
|
||||||
@ -46,6 +164,15 @@ pub trait WebComponentDef: IntoWasmAbi + Default {
|
|||||||
/// Each method is optional. You only need to implement the ones
|
/// Each method is optional. You only need to implement the ones
|
||||||
/// you want to specify behavior for.
|
/// you want to specify behavior for.
|
||||||
pub trait WebComponentBinding: WebComponentDef {
|
pub trait WebComponentBinding: WebComponentDef {
|
||||||
|
/// Called during element construction.
|
||||||
|
fn init(&self, _element: &HtmlElement) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_mut(&mut self, _element: &HtmlElement) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when the web component is connected to the DOM.
|
/// Called when the web component is connected to the DOM.
|
||||||
/// This is when you should do any setup like attaching a ShadowDom
|
/// This is when you should do any setup like attaching a ShadowDom
|
||||||
/// or appending elements.
|
/// or appending elements.
|
||||||
@ -53,16 +180,33 @@ pub trait WebComponentBinding: WebComponentDef {
|
|||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when the web component is connected to the DOM.
|
||||||
|
/// This is when you should do any setup like attaching a ShadowDom
|
||||||
|
/// or appending elements.
|
||||||
|
fn connected_mut(&mut self, _element: &HtmlElement) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when the web component is disconnected from the DOM.
|
/// Called when the web component is disconnected from the DOM.
|
||||||
fn disconnected(&self, _element: &HtmlElement) {
|
fn disconnected(&self, _element: &HtmlElement) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when the web component is disconnected from the DOM.
|
||||||
|
fn disconnected_mut(&mut self, _element: &HtmlElement) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
/// Called When the web component is moved to a new document.
|
/// Called When the web component is moved to a new document.
|
||||||
fn adopted(&self, _element: &HtmlElement) {
|
fn adopted(&self, _element: &HtmlElement) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called When the web component is moved to a new document.
|
||||||
|
fn adopted_mut(&mut self, _element: &HtmlElement) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when one of the observed attributes has changed.
|
/// Called when one of the observed attributes has changed.
|
||||||
/// the observedc attributes are listed in the observed_attrs argument to the
|
/// the observedc attributes are listed in the observed_attrs argument to the
|
||||||
/// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute.
|
/// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute.
|
||||||
@ -76,16 +220,46 @@ pub trait WebComponentBinding: WebComponentDef {
|
|||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top level event handler for this custome element.
|
/// Called when one of the observed attributes has changed.
|
||||||
|
/// the observedc attributes are listed in the observed_attrs argument to the
|
||||||
|
/// `#[web_component(observed_attrs = "['attr1', 'attr2']")` attribute.
|
||||||
|
fn attribute_changed_mut(
|
||||||
|
&mut self,
|
||||||
|
_element: &HtmlElement,
|
||||||
|
_name: JsValue,
|
||||||
|
_old_value: JsValue,
|
||||||
|
_new_value: JsValue,
|
||||||
|
) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top level event handler for this custom element.
|
||||||
fn handle_event(&self, _element: &HtmlElement, _event: &Event) {
|
fn handle_event(&self, _element: &HtmlElement, _event: &Event) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Top level event handler for this custom element.
|
||||||
|
fn handle_event_mut(&mut self, _element: &HtmlElement, _event: &Event) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker trait used in the generated shims to assert that their are Rust implemtntations
|
/// Marker trait used in the generated shims to assert that there are Rust implemtntations
|
||||||
/// of the callback functions for the component.
|
/// of the callback functions for the component.
|
||||||
pub trait WebComponent: WebComponentBinding {}
|
pub trait WebComponent: WebComponentBinding {}
|
||||||
|
|
||||||
|
/// Defines the template element rendering method.
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
pub trait TemplateElementRender {
|
||||||
|
// Creates and returns an HtmlTemplateElement.
|
||||||
|
fn render() -> HtmlTemplateElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker trait used in the generated shims to assert that there are Rust implemtntations
|
||||||
|
/// of the rendering function for the component.
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
pub trait TemplateElement: TemplateElementRender {}
|
||||||
|
|
||||||
/// A handle for your WebComponent Definition. Offers easy access to construct your
|
/// A handle for your WebComponent Definition. Offers easy access to construct your
|
||||||
/// element.
|
/// element.
|
||||||
pub struct WebComponentHandle {
|
pub struct WebComponentHandle {
|
||||||
@ -191,7 +365,7 @@ mod tests {
|
|||||||
#[web_component(
|
#[web_component(
|
||||||
class_name = "MyElement",
|
class_name = "MyElement",
|
||||||
element_name = "my-element",
|
element_name = "my-element",
|
||||||
observed_attrs = "['class']"
|
observed_attrs = "['class']",
|
||||||
)]
|
)]
|
||||||
pub struct MyElementImpl {}
|
pub struct MyElementImpl {}
|
||||||
|
|
||||||
@ -270,9 +444,96 @@ mod tests {
|
|||||||
"Added a text node on adopt"
|
"Added a text node on adopt"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Then we can have the new document adopt this node.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[wasm_bindgen_test]
|
||||||
fn test_component_no_element_name() {
|
fn test_component_no_element_name() {
|
||||||
#[web_component(class_name = "AnElement")]
|
#[web_component(class_name = "AnElement")]
|
||||||
@ -301,4 +562,36 @@ mod tests {
|
|||||||
assert_eq!(ThisElement::class_name(), "ThisElement");
|
assert_eq!(ThisElement::class_name(), "ThisElement");
|
||||||
assert_eq!(ThisElement::element_name(), "this-old-element");
|
assert_eq!(ThisElement::element_name(), "this-old-element");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jwall): Tests for event handling
|
||||||
|
|
||||||
|
// TODO(jwall): Benchmarks for TemplateElements?
|
||||||
|
#[cfg(feature = "HtmlTemplateElement")]
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_template_element_render_once() {
|
||||||
|
use wasm_web_component_macros::template_element;
|
||||||
|
|
||||||
|
#[template_element]
|
||||||
|
pub struct MyTemplate();
|
||||||
|
impl TemplateElementRender for MyTemplate {
|
||||||
|
fn render() -> HtmlTemplateElement {
|
||||||
|
let val: JsValue = window()
|
||||||
|
.unwrap()
|
||||||
|
.document()
|
||||||
|
.unwrap()
|
||||||
|
.create_element("template")
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
let el: HtmlTemplateElement = val.into();
|
||||||
|
el.set_attribute("id", "template-id").unwrap();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = window().unwrap().document().unwrap().body().unwrap();
|
||||||
|
assert!(!body.last_child().unwrap().has_type::<HtmlTemplateElement>());
|
||||||
|
let id = MyTemplate::define_once();
|
||||||
|
assert_eq!(id.unwrap(), &Some(String::from("template-id")));
|
||||||
|
assert!(body.last_child().unwrap().has_type::<HtmlTemplateElement>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user