sauron_component_macro/
lib.rs

1use quote::quote;
2
3#[proc_macro_attribute]
4pub fn custom_element(
5    attr: proc_macro::TokenStream,
6    item: proc_macro::TokenStream,
7) -> proc_macro::TokenStream {
8    let custom_tag: proc_macro2::Literal =
9        syn::parse(attr).expect("must be a literal");
10    let impl_item: syn::ItemImpl = syn::parse(item)
11        .expect("Expecting custom_element macro to be used in impl trait");
12
13    let (_, path, _) = &impl_item
14        .trait_
15        .as_ref()
16        .expect("must have a trait implementation");
17
18    let component: &syn::PathSegment =
19        &path.segments.last().expect("must have a last segment");
20
21    let component_ident = component.ident.to_string();
22
23    match &*component_ident {
24        "Component" => impl_component(&impl_item, &custom_tag, component),
25        "Application" => impl_application(&impl_item, &custom_tag, component),
26        _ => panic!("unsupported trait implementation: {}", component_ident),
27    }
28}
29
30/// Generate the component registration for component that has a Component trait implementation
31fn impl_component(
32    impl_item: &syn::ItemImpl,
33    custom_tag: &proc_macro2::Literal,
34    component: &syn::PathSegment,
35) -> proc_macro::TokenStream {
36    let mut tokens = proc_macro2::TokenStream::new();
37    let component_msg = get_msg_generic_ident(&component);
38    let self_type = &impl_item.self_ty;
39    if let syn::Type::Path(type_path) = self_type.as_ref() {
40        let path_segment = &type_path.path.segments[0];
41        let component = &path_segment.ident;
42        let derive_component = proc_macro2::Ident::new(
43            &format!("_{}__CustomElement", component),
44            proc_macro2::Span::call_site(),
45        );
46        let derive_msg = proc_macro2::Ident::new(
47            &format!("_{}__CustomMsg", component),
48            proc_macro2::Span::call_site(),
49        );
50
51        let derive_component_str = derive_component.to_string();
52
53        tokens.extend(quote! {
54
55            #impl_item
56
57            #[allow(non_camel_case_types)]
58            pub struct #derive_msg(#component_msg);
59
60            #[allow(non_camel_case_types)]
61            #[wasm_bindgen]
62            pub struct #derive_component{
63                program: Program<#component<#derive_msg>, #derive_msg>,
64            }
65
66            #[wasm_bindgen]
67            impl #derive_component {
68                #[wasm_bindgen(constructor)]
69                pub fn new(node: JsValue) -> Self {
70                    use sauron::wasm_bindgen::JsCast;
71                    log::info!("constructor..");
72                    let mount_node: &web_sys::Node = node.unchecked_ref();
73                    Self {
74                        program: Program::new(
75                            #component::default(),
76                            mount_node,
77                            false,
78                            true,
79                        ),
80                    }
81                }
82
83                #[wasm_bindgen(method)]
84                pub fn observed_attributes() -> JsValue {
85                    JsValue::from_serde(&<#component::<#derive_msg> as Component<#component_msg, #derive_msg>>::observed_attributes())
86                        .expect("must parse from serde")
87                }
88
89                #[wasm_bindgen(method)]
90                pub fn attribute_changed_callback(&self) {
91                    use std::ops::DerefMut;
92                    use sauron::wasm_bindgen::JsCast;
93                    log::info!("attribute changed...");
94                    let mount_node = self.program.mount_node();
95                    let mount_element: &web_sys::Element = mount_node.unchecked_ref();
96                    let attribute_names = mount_element.get_attribute_names();
97                    let len = attribute_names.length();
98                    let mut attribute_values: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
99                    for i in 0..len {
100                        let name = attribute_names.get(i);
101                        let attr_name =
102                            name.as_string().expect("must be a string attribute");
103                        if let Some(attr_value) = mount_element.get_attribute(&attr_name) {
104                            attribute_values.insert(attr_name, attr_value);
105                        }
106                    }
107                    <#component<#derive_msg> as Component<#component_msg, #derive_msg>>::attributes_changed(self.program.app.borrow_mut().deref_mut(), attribute_values);
108                }
109
110                #[wasm_bindgen(method)]
111                pub fn connected_callback(&mut self) {
112                    use std::ops::Deref;
113                    self.program.mount();
114                    log::info!("Component is connected..");
115                    let component_style = <#component<#derive_msg> as Component<#component_msg, #derive_msg>>::style(self.program.app.borrow().deref());
116                    self.program.inject_style_to_mount(&component_style);
117                    self.program.update_dom();
118                }
119                #[wasm_bindgen(method)]
120                pub fn disconnected_callback(&mut self) {
121                    log::info!("Component is disconnected..");
122                }
123                #[wasm_bindgen(method)]
124                pub fn adopted_callback(&mut self) {
125                    log::info!("Component is adopted..");
126                }
127
128            }
129
130            impl Application<#derive_msg> for #component<#derive_msg> {
131                fn update(&mut self, msg: #derive_msg) -> Cmd<Self, #derive_msg> {
132                    let mount_attributes = <Self as Component<#component_msg, #derive_msg>>::attributes_for_mount(self);
133                    Cmd::batch([
134                        Cmd::from(
135                            <Self as Component<#component_msg, #derive_msg>>::update(
136                                self, msg.0,
137                            )
138                            .localize(#derive_msg),
139                        ),
140                        Cmd::new(|program| {
141                            program.update_mount_attributes(mount_attributes);
142                        }),
143                    ])
144                }
145
146                fn view(&self) -> Node<#derive_msg> {
147                    <Self as Component<#component_msg, #derive_msg>>::view(self)
148                        .map_msg(#derive_msg)
149                }
150            }
151
152            #[wasm_bindgen]
153            pub fn register(){
154                sauron::register_custom_element(#custom_tag, #derive_component_str, "HTMLElement");
155            }
156
157        });
158    } else {
159        panic!("Expecting a Path");
160    }
161    tokens.into()
162}
163
164/// Generate the code for registering the application as a custom component
165fn impl_application(
166    impl_item: &syn::ItemImpl,
167    custom_tag: &proc_macro2::Literal,
168    component: &syn::PathSegment,
169) -> proc_macro::TokenStream {
170    let mut tokens = proc_macro2::TokenStream::new();
171    let app_msg = get_msg_generic_ident(&component);
172    let self_type = &impl_item.self_ty;
173    if let syn::Type::Path(type_path) = self_type.as_ref() {
174        let path_segment = &type_path.path.segments[0];
175        let app = &path_segment.ident;
176        let derive_component = proc_macro2::Ident::new(
177            &format!("_{}__CustomElement", app),
178            proc_macro2::Span::call_site(),
179        );
180
181        let derive_component_str = derive_component.to_string();
182
183        tokens.extend(quote! {
184
185            #impl_item
186
187            #[allow(non_camel_case_types)]
188            #[wasm_bindgen]
189            pub struct #derive_component{
190                program: Program<#app, #app_msg>,
191            }
192
193            #[wasm_bindgen]
194            impl #derive_component {
195                #[wasm_bindgen(constructor)]
196                pub fn new(node: JsValue) -> Self {
197                    use sauron::wasm_bindgen::JsCast;
198                    log::info!("constructor..");
199                    let mount_node: &web_sys::Node = node.unchecked_ref();
200                    Self {
201                        program: Program::new(
202                            #app::default(),
203                            mount_node,
204                            false,
205                            true,
206                        ),
207                    }
208                }
209
210                #[wasm_bindgen(method)]
211                pub fn observed_attributes() -> JsValue {
212                    JsValue::from_serde(&<#app as Application<#app_msg>>::observed_attributes())
213                        .expect("must parse from serde")
214                }
215
216                #[wasm_bindgen(method)]
217                pub fn attribute_changed_callback(&self) {
218                    use sauron::wasm_bindgen::JsCast;
219                    log::info!("attribute changed...");
220                    let mount_node = self.program.mount_node();
221                    let mount_element: &web_sys::Element = mount_node.unchecked_ref();
222                    let attribute_names = mount_element.get_attribute_names();
223                    let len = attribute_names.length();
224                    let mut attribute_values: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
225                    for i in 0..len {
226                        let name = attribute_names.get(i);
227                        let attr_name =
228                            name.as_string().expect("must be a string attribute");
229                        if let Some(attr_value) = mount_element.get_attribute(&attr_name) {
230                            attribute_values.insert(attr_name, attr_value);
231                        }
232                    }
233                    self.program
234                        .app
235                        .borrow_mut()
236                        .attributes_changed(attribute_values);
237                }
238
239                #[wasm_bindgen(method)]
240                pub fn connected_callback(&mut self) {
241                    use std::ops::Deref;
242                    self.program.mount();
243                    log::info!("Application is connected..");
244                    let app_style = self.program.app.borrow().style();
245                    self.program.inject_style_to_mount(&app_style);
246                    self.program.update_dom();
247                }
248                #[wasm_bindgen(method)]
249                pub fn disconnected_callback(&mut self) {
250                    log::info!("Application is disconnected..");
251                }
252                #[wasm_bindgen(method)]
253                pub fn adopted_callback(&mut self) {
254                    log::info!("Application is adopted..");
255                }
256
257            }
258
259            #[wasm_bindgen]
260            pub fn register_application(){
261                sauron::register_custom_element(#custom_tag, #derive_component_str, "HTMLElement");
262            }
263
264        });
265    } else {
266        panic!("Expecting a Path");
267    }
268    tokens.into()
269}
270
271fn get_msg_generic_ident(component: &syn::PathSegment) -> proc_macro2::Ident {
272    let component_msg =
273        if let syn::PathArguments::AngleBracketed(component_msg) =
274            &component.arguments
275        {
276            let first_arg_generics = &component_msg.args[0];
277            if let syn::GenericArgument::Type(type_) = first_arg_generics {
278                if let syn::Type::Path(type_path) = type_ {
279                    let generic = type_path
280                        .path
281                        .segments
282                        .last()
283                        .expect("must have a generic path segment");
284                    generic.ident.clone()
285                } else {
286                    panic!("expecting a type path");
287                }
288            } else {
289                panic!("expecting a generic argument type");
290            }
291        } else {
292            panic!("expecting a generic argument");
293        };
294    component_msg
295}