Clone in a terminal:
Commit 2d79b82
Johan Mattsson - 2026-07-14T20:42:14+00:00
2d79b826e409887f20a7378f0c4523f9ed83beccjohan.mattsson.m@gmail.com

New component macro

Changed files (2)

Diff

diff --git a/Cargo.toml b/Cargo.tomlindex 0f23d60..ef95b4d 100644--- a/Cargo.toml+++ b/Cargo.toml@@ -15,7 +15,7 @@ proc-macro2 = "1" gloo = "0.12.0" wasm-bindgen = "0.2.122" js-sys = "0.3"-web-sys = { version = "0.3.99", features = ["Window", "HtmlElement", "CustomElementRegistry", "ElementDefinitionOptions"] }+web-sys = { version = "0.3.99", features = ["Window", "Document", "HtmlDocument", "Element", "HtmlElement", "HtmlInputElement", "NodeList", "CssStyleDeclaration", "DomTokenList", "MessageEvent", "CustomElementRegistry", "ElementDefinitionOptions"] } inventory = "0.3.24"  [dev-dependencies]diff --git a/src/init.rs b/src/init.rsindex 471dca0..0cdebb3 100644--- a/src/init.rs+++ b/src/init.rs@@ -2,7 +2,7 @@ use proc_macro as pm; use proc_macro2::Span; use quote::quote; use syn::parse2;-use syn::{Ident, ItemStruct, LitStr};+use syn::{Fields, Ident, ItemStruct, LitStr};  pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::TokenStream {     // Parse this #[component("my-tag")]) for a struct@@ -37,8 +37,6 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token      let struct_name_ident = &component_struct_item.ident; -    let global_store = generate_component_store(struct_name_ident.clone());-     let create_class_js = format!("createComponentClass{}", struct_name_ident);     let init_self_js = format!("initSelf{}", struct_name_ident);     let attach_js = format!("attachComponent{}", struct_name_ident);@@ -66,55 +64,37 @@ pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::Token     let observed_fn = observed_quote(&observed_js, struct_name_ident);     let changed_fn = changed_quote(&changed_js, struct_name_ident);     let component_impl = component_impl_quote(struct_name_ident);+    let component_full_impl = create_component(&component_struct_item);      let expanded = quote! {-        #global_store-        #rs_self         #component_struct_item+        #component_impl+        #component_full_impl+         #create_js_class_fn++        #rs_self+         #attach_fn         #detach_fn         #observed_fn         #changed_fn-        #component_impl     };      expanded.into() } -fn storage_for_struct_impl(struct_name: &Ident) -> proc_macro2::TokenStream {-    let store_name_str = store_name(struct_name);-    let store_ident = Ident::new(&store_name_str, Span::call_site());--    let expanded = quote! {-        impl ::kaja_web::ComponentStorage for #struct_name {-            fn storage() -> &'static std::thread::LocalKey<-                std::cell::RefCell<-                    std::collections::HashMap<String, Rc<RefCell<#struct_name>>>,-                >,-            > {-                &#store_ident-            }-        }-    };--    expanded-}- fn attach_quote(atach_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::TokenStream {     let js_callback_ident = Ident::new(atach_state_js_fn, Span::call_site());     let rs_name_str = from_pascal_to_snake(atach_state_js_fn);     let rs_name_ident = Ident::new(&rs_name_str, Span::call_site());-    let store_name_str = store_name(struct_name);-    let store_ident = Ident::new(&store_name_str, Span::call_site());+    let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site());      let expanded = quote! {         #[callback(#js_callback_ident)]         fn #rs_name_ident(id: String) {-            let exists = #store_ident.with(|store| store.borrow().contains_key(&id));--            if exists {-                #struct_name::connected(&id);+            if let Some(component) = #component_name::get_component(&id) {+                component.connected();             } else {                 error!("Component id not found in connect.", id.clone());             }@@ -131,14 +111,13 @@ fn detach_quote(detach_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::T      let store_name_str = store_name(struct_name);     let store_ident = Ident::new(&store_name_str, Span::call_site());+    let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site());      let expanded = quote! {         #[callback(#js_callback_ident)]         fn #rs_name_ident(wasm_id: String) {-            let exists = #store_ident.with(|store| store.borrow().contains_key(&wasm_id));--            if exists {-                #struct_name::disconnected(&wasm_id);+            if let Some(component) = #component_name::get_component(&wasm_id) {+                component.disconnected();                 #store_ident.with(|store| {                     store.borrow_mut().remove(&wasm_id);                 });@@ -173,18 +152,15 @@ fn changed_quote(changed_state_js_fn: &str, struct_name: &Ident) -> proc_macro2:     let js_callback_ident = Ident::new(changed_state_js_fn, Span::call_site());     let rs_name_str = from_pascal_to_snake(changed_state_js_fn);     let rs_name_ident = Ident::new(&rs_name_str, Span::call_site());--    let store_name_str = store_name(struct_name);-    let store_ident = Ident::new(&store_name_str, Span::call_site());+    let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site());      let expanded = quote! {         #[callback(#js_callback_ident)]         fn #rs_name_ident(element_id: String, key: String, from: String, to: String) {-            let exists = #store_ident.with(|store| store.borrow().contains_key(&element_id));--            if exists {+            if let Some(component) = #component_name::get_component(&element_id) {                 let name = key.as_str();-                #struct_name::attribute_changed(&element_id, name, &from, &to);+                component.internal_attributes_update(name, &from, &to);+                component.attribute_changed(name, &from, &to);             } else {                 error!("Component id not found in changed attribute.", element_id.clone());             }@@ -198,13 +174,12 @@ fn observed_quote(observed_js_fn: &str, struct_name: &Ident) -> proc_macro2::Tok     let js_callback_ident = Ident::new(observed_js_fn, Span::call_site());     let rs_name_str = from_pascal_to_snake(observed_js_fn);     let rs_name_ident = Ident::new(&rs_name_str, Span::call_site());--    let struct_ident = struct_name;+    let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site());      let expanded = quote! {         #[callback(#js_callback_ident)]         fn #rs_name_ident() -> &'static [&'static str] {-            #struct_ident::observed_attributes()+            #component_name::internal_observed_attributes()         }     }; @@ -216,37 +191,6 @@ fn store_name(struct_name: &Ident) -> String {     store_name } -fn generate_component_store(struct_name: Ident) -> proc_macro2::TokenStream {-    // Generate only the per-component store (HashMap) here.-    let store_name = store_name(&struct_name);-    let store_ident = Ident::new(&store_name, Span::call_site());-    let store_hash_map_impl = storage_for_struct_impl(&struct_name);--    let expanded = quote! {-        std::thread_local! {-            pub static #store_ident: std::cell::RefCell<std::collections::HashMap<String, ::kaja_web::SharedComponent<#struct_name>>> =-                std::cell::RefCell::new(std::collections::HashMap::new());-        }-        #store_hash_map_impl--        impl #struct_name {-            pub fn get_component(-                component_id: &str,-            ) -> Option<Rc<RefCell<#struct_name>>> {-                #store_ident.with(|store| store.borrow().get(component_id).cloned())-            }--            pub fn component_exists(-                component_id: &str,-            ) -> bool {-                #store_ident.with(|store| store.borrow().contains_key(component_id))-            }-        }-    };--    expanded-}- fn from_pascal_to_snake(fn_name: &str) -> String {     let mut result = String::new(); @@ -283,11 +227,15 @@ fn gen_rust_self(struct_item: ItemStruct, js_name: Ident) -> proc_macro2::TokenS     kebab_case_name = kebab_case_name.replace("_", "-");      let rust_ident = Ident::new(&rust_name, Span::call_site());+    let struct_ref_container =+        Ident::new(&format!("{}RefContainer", struct_ident), Span::call_site());      let expanded = quote! {         #[callback(#js_name)]         #[allow(non_snake_case)]         pub fn #rust_ident(current_id: String) -> String {  // init_component_state_struct_name, returns id for the instance of the element+            log!("Compoent generation previous id:", current_id.clone());+             let #var_ident = #struct_ident::default();              let key = if current_id.is_empty() {@@ -297,11 +245,17 @@ fn gen_rust_self(struct_item: ItemStruct, js_name: Ident) -> proc_macro2::TokenS                 current_id             }; +            let component = std::rc::Rc::new(std::cell::RefCell::new(#struct_ref_container {+                id: key.clone(),+                attribute_values: std::rc::Rc::new(std::cell::RefCell::new(#var_ident)),+            }));+             #store_ident.with(|store| {-                store-                    .borrow_mut()-                    .insert(key.clone(), std::rc::Rc::new(std::cell::RefCell::new(#var_ident)));+                store.borrow_mut().insert(key.clone(), component);             });++            log!("created key:", key.clone());+             key         }     };@@ -333,42 +287,42 @@ fn create_class(              let js_init = String::from(                 r#"-                    return class extends HTMLElement {-                        constructor() {-                            super();-                            let currentId = this.getAttribute("id");+                        return class extends HTMLElement {+                            constructor() {+                                super();+                                let currentId = this.getAttribute("id"); -                            if (currentId === null) {-                                currentId = "";-                            }+                                if (currentId === null) {+                                    currentId = "";+                                } -                            let resolvedId = __INIT_SELF_CALLBACK(currentId);+                                let resolvedId = __INIT_SELF_CALLBACK(currentId); -                            if (!currentId) {-                                this.setAttribute("id", resolvedId);+                                if (!currentId) {+                                    this.setAttribute("id", resolvedId);+                                }                             }-                        } -                        connectedCallback() {-                            let componentid = this.getAttribute("id");-                            __ATTACH_CALLBACK(componentid);-                        }+                            connectedCallback() {+                                let componentid = this.getAttribute("id");+                                __ATTACH_CALLBACK(componentid);+                            } -                        disconnectedCallback() {-                            let componentid = this.getAttribute("id");-                            __DETACH_CALLBACK(componentid);-                        }+                            disconnectedCallback() {+                                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));-                        }+                            attributeChangedCallback(name, oldValue, 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();+                            static get observedAttributes() {+                                return __OBSERVED_CALLBACK();+                            }                         }-                    }-            "#,+                "#,             );              let js_init_with_state = js_init.replace("__INIT_SELF_CALLBACK", #init_self_fn);@@ -398,3 +352,186 @@ fn create_class(      expanded }++fn create_component(struct_item: &ItemStruct) -> proc_macro2::TokenStream {+    let struct_ident = &struct_item.ident;+    let struct_ref_container =+        Ident::new(&format!("{}RefContainer", struct_ident), Span::call_site());+    let component_ident = Ident::new(&format!("{}Component", struct_ident), Span::call_site());++    let store_name_str = store_name(struct_ident);+    let store_ident = Ident::new(&store_name_str, Span::call_site());++    let (setter_arms, getter_arms, observed_attributes) = match &struct_item.fields {+        Fields::Named(fields) => {+            let setter_arms = fields.named.iter().filter_map(|field| {+                let field_ident = field.ident.as_ref()?;+                let field_name = field_ident.to_string();+                let field_ty = &field.ty;+                Some(quote! {+                    #field_name => {+                        self.#field_ident = value.parse::<#field_ty>().map_err(|_| ::kaja_web::Error)?;+                    }+                })+            });++            let getter_arms = fields.named.iter().filter_map(|field| {+                let field_ident = field.ident.as_ref()?;+                let field_name = field_ident.to_string();+                Some(quote! {+                    #field_name => Some(self.#field_ident.to_string())+                })+            });++            let observed_attributes = fields.named.iter().filter_map(|field| {+                let field_ident = field.ident.as_ref()?;+                let field_name = LitStr::new(&field_ident.to_string(), Span::call_site());+                Some(quote! { #field_name })+            });++            (+                quote! { #(#setter_arms,)* },+                quote! { #(#getter_arms,)* },+                quote! { #(#observed_attributes,)* },+            )+        }+        _ => (quote! {}, quote! {}, quote! {}),+    };++    let struct_name_lit = LitStr::new(&struct_ident.to_string(), Span::call_site());++    let expanded = quote! {+        thread_local! {+            static #store_ident: std::cell::RefCell<std::collections::HashMap<String, std::rc::Rc<std::cell::RefCell<#struct_ref_container>>>> =+                std::cell::RefCell::new(std::collections::HashMap::new());+        }++        impl #struct_ident {+            fn set_test_attribute(&mut self, name: &str, value: &str) {+                let _ = self.try_set_test_attribute(name, value);+            }++            fn try_set_test_attribute(&mut self, name: &str, value: &str) -> ::kaja_web::Result<()> {+                match name {+                    #setter_arms+                    _ => {+                        error!("No field", name, "in", #struct_name_lit, "can't set it to", value);+                        return Err(::kaja_web::Error);+                    }+                }++                Ok(())+            }++            fn get_attribute(&self, name: &str) -> Option<String> {+                match name {+                    #getter_arms+                    _ => {+                        error!("No field", name, "in", #struct_name_lit, "can't get it");+                        None+                    }+                }+            }+        }++        #[derive(Default)]+        struct #struct_ref_container {+            id: String,+            attribute_values: std::rc::Rc<std::cell::RefCell<#struct_ident>>,+        }++        impl #struct_ref_container {+            fn create_component(id: &str) {+                let handle = std::rc::Rc::new(std::cell::RefCell::new(#struct_ref_container {+                    id: id.to_string(),+                    attribute_values: std::rc::Rc::new(std::cell::RefCell::new(#struct_ident::default())),+                }));++                Self::register_component(handle.clone());+                let component = #component_ident::get_component(&id);++                if component.is_none() {+                    error!("Failed to register component.");+                    return;+                }++                let component_impl = component.unwrap();+                component_impl.connected();+            }++            fn register_component(component: std::rc::Rc<std::cell::RefCell<#struct_ref_container>>) {+                let id = component.borrow().id.clone();++                #store_ident.with(|components| {+                    components.borrow_mut().insert(id, component);+                });+            }+        }++        #[derive(Default)]+        struct #component_ident {+            handle: std::rc::Rc<std::cell::RefCell<#struct_ref_container>>,+        }++        impl #component_ident {+            fn get_component(id: &str) -> Option<#component_ident> {+                let handle = #store_ident.with(|components| components.borrow().get(id).cloned());++                if !handle.is_some() {+                    return None;+                }++                Some(#component_ident {+                    handle: handle.unwrap(),+                })+            }++            fn internal_observed_attributes() -> &'static [&'static str] {+                &[#observed_attributes]+            }++            fn internal_attributes_update(&self, name: &str, _old_value: &str, new_value: &str) {+                let handle = self.handle.borrow();+                let mut attr = handle.attribute_values.borrow_mut();+                attr.set_test_attribute(name, new_value);+            }++            fn set_attribute(&self, name: &str, value: &str) {+                let id = self.get_id();+                let element = get_element_by_id(&id);++                if element.is_none() {+                    error!("Element not found for id:", id, "in set_attribute");+                    return;+                }++                if !Self::internal_observed_attributes().contains(&name) {+                    error!("Attribute not observed:", name, "in set_attribute");+                    return;+                }++                let res = element.unwrap().set_attribute(name, value);++                if res.is_err() {+                    error!("Failed to set attribute:", name, "in set_attribute");+                }+            }++            fn get_attribute(&self, name: &str) -> Option<String> {+                let handle = self.handle.borrow();+                let attr = handle.attribute_values.borrow();+                attr.get_attribute(name)+            }++            fn get_attributes(&self) -> std::rc::Rc<std::cell::RefCell<#struct_ident>> {+                self.handle.borrow().attribute_values.clone()+            }++            fn get_id(&self) -> String {+                self.handle.borrow().id.clone()+            }+        }+    };++    expanded+}

Contact

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