sauron_component_macro/
lib.rs1use 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
30fn 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
164fn 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}