Clone in a terminal:
Commit 41df0ed
Johan Mattsson - 2026-07-14T22:08:32+00:00
41df0edfbb8b6ebd47c72ada4c5864c07d15f346johan.mattsson.m@gmail.com

Validate attributes set on web component in JS

Changed files (1)

Diff

diff --git a/src/init.rs b/src/init.rsindex 0d668bf..9ce8e86 100644--- a/src/init.rs+++ b/src/init.rs@@ -43,6 +43,7 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token     let detach_js = format!("detachComponent{}", struct_ident);     let observed_js = format!("observedComponentAttribute{}", struct_ident);     let changed_js = format!("changedComponentAttribute{}", struct_ident);+    let validate_attribute_js = format!("validateAttribute{}", struct_ident);      let create_class_js_indent = Ident::new(&create_class_js, Span::call_site());     let init_self_js_ident = Ident::new(&init_self_js, Span::call_site());@@ -55,6 +56,7 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token         detach_js.as_str(),         observed_js.as_str(),         changed_js.as_str(),+        validate_attribute_js.as_str(),     );      let rs_self = gen_rust_self(component_struct_item.clone(), init_self_js_ident);@@ -64,6 +66,7 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token     let changed_fn = changed_quote(&changed_js, struct_ident);     let component_impl = component_impl_quote(struct_ident);     let component_full_impl = create_component(&component_struct_item);+    let validate_attribute_fn = validate_attribute_quote(&validate_attribute_js, struct_ident);      let expanded = quote! {         #[allow(dead_code, unreachable_code)]@@ -79,6 +82,7 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token         #detach_fn         #observed_fn         #changed_fn+        #validate_attribute_fn     };      expanded.into()@@ -132,6 +136,31 @@ fn detach_quote(detach_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::T     expanded } +fn validate_attribute_quote(+    validate_attribute_js: &str,+    struct_name: &Ident,+) -> proc_macro2::TokenStream {+    let js_callback_ident = Ident::new(validate_attribute_js, Span::call_site());+    let rs_name_str = from_pascal_to_snake(validate_attribute_js);+    let rs_name_ident = Ident::new(&rs_name_str, Span::call_site());+    let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site());++    let expanded = quote! {+        #[allow(unreachable_code)]+        #[callback(#js_callback_ident)]+        fn #rs_name_ident(wasm_id: String, name: String, value: String) -> bool {+            if let Some(component) = #component_name::get_component(&wasm_id) {+                return component.valid_attribute(name.as_str(), value.as_str());+            }++            error!("No component found for", wasm_id.clone());+            false+        }+    };++    expanded+}+ fn component_impl_quote(struct_name: &Ident) -> proc_macro2::TokenStream {     let snake_name = from_pascal_to_snake(&struct_name.to_string());     let init_fn = format!("{}_init_component", snake_name);@@ -278,6 +307,7 @@ fn create_class(     detach_state_fn: &str,     observed_fn: &str,     change_fn: &str,+    validate_attribute_fn: &str, ) -> proc_macro2::TokenStream {     let name_str_lit = LitStr::new(&class_creator.to_string(), Span::call_site()); @@ -312,23 +342,39 @@ fn create_class(                             }                              connectedCallback() {-                                let componentid = this.getAttribute("id");-                                __ATTACH_CALLBACK(componentid);+                                let componentId = this.getAttribute("id");+                                __ATTACH_CALLBACK(componentId);                             }                              disconnectedCallback() {-                                let componentid = this.getAttribute("id");-                                __DETACH_CALLBACK(componentid);+                                let componentId = this.getAttribute("id");+                                __DETACH_CALLBACK(componentId);                             }                              attributeChangedCallback(name, oldValue, newValue) {-                                let componentid = this.getAttribute("id");-                                __ATTRIBUTE_CHANGED_CALLBACK(componentid, name, oldValue === null ? "" : String(oldValue), newValue === null ? "" : String(newValue));+                                let componentId = this.getAttribute("id");+                                __ATTRIBUTE_CHANGED_CALLBACK(componentId, name, oldValue === null ? "" : String(oldValue), newValue === null ? "" : String(newValue));                             }                              static get observedAttributes() {                                 return __OBSERVED_CALLBACK();                             }++                            setAttribute(name, value) {+                                let componentId = this.getAttribute("id");++                                if (name === "id" || componentId === null || componentId === "") {+                                    return super.setAttribute(name, value);+                                }++                                let valAsStr = String(value);++                                if (!__VALIDATE_ATTRIBUTE(componentId, name, valAsStr)) {+                                    throw new TypeError(`"${value}" is not a valid value for "${name}"`);+                                }++                                return super.setAttribute(name, value);+                            }                         }                 "#,             );@@ -338,6 +384,7 @@ fn create_class(             let js_init_with_state = js_init_with_state.replace("__DETACH_CALLBACK", #detach_state_fn);             let js_init_with_state = js_init_with_state.replace("__ATTRIBUTE_CHANGED_CALLBACK", #change_fn);             let js_init_with_state = js_init_with_state.replace("__OBSERVED_CALLBACK", #observed_fn);+            let js_init_with_state = js_init_with_state.replace("__VALIDATE_ATTRIBUTE", #validate_attribute_fn);              let component = js_sys::Function::new_no_args(js_init_with_state.as_str()); 

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112