mogwai_macros/
lib.rs

1//! RSX for constructing `web-sys` elements.
2#![allow(deprecated)]
3
4use quote::{ToTokens, quote};
5use syn::spanned::Spanned;
6
7mod tokens;
8
9#[proc_macro]
10/// View construction macro.
11///
12/// The `rsx!` macro facilitates the creation of UI components using a syntax
13/// similar to JSX, allowing for a more intuitive and declarative way to define
14/// views in Rust.
15///
16/// This macro transforms a tree of HTML-like syntax into Rust code that constructs
17/// the corresponding UI elements. It supports `let` binding, embedding Rust expressions,
18/// and handling events, making it a powerful tool for building dynamic interfaces.
19///
20/// # Examples
21///
22/// ## Basic Usage
23///
24/// ```rust
25/// use mogwai::prelude::*;
26///
27/// fn view<V:View>() -> V::Element {
28///     rsx! {
29///         let root = div(class = "container") {
30///             h1 { "Hello, World!" }
31///             button(on:click = handle_click) { "Click me" }
32///         }
33///     }
34///
35///     root
36/// }
37/// ```
38///
39/// In this example, `rsx!` is used to create a `div` with a class and two child
40/// elements: an `h1` and a `button` with an event listener `handle_click`. The root
41/// `div` element is bound with a let binding to the name `root`.
42///
43/// ## Attributes
44///
45/// In addition to single-word attributes, view nodes support a few special attributes:
46///
47/// - **on:** Used to attach event listeners.
48///   For example, `on:click = handle_click` attaches a click event listener named `handle_click`.
49/// - **window:** Used to attach event listeners to the window object.
50///   For example, `window:resize = handle_resize`.
51/// - **document:** Used to attach event listeners to the document object.
52///   For example, `document:keydown = handle_keydown`.
53/// - **style:** Shorthand used to set inline styles.
54///   For example, `style:color = "red"` sets the text color to red, and is equivalent to
55///   `style = "color: red;"`.
56///
57/// ## Using `Proxy`
58///
59/// The `rsx!` macro includes special shorthand syntax for dynamic updates using `Proxy`.
60/// This syntax is valid in both attribute and node positions.
61///
62/// ```rust
63/// use mogwai::ssr::prelude::*;
64///
65/// #[derive(Debug, PartialEq)]
66/// struct Status {
67///     color: String,
68///     message: String,
69/// }
70///
71/// struct Widget<V: View> {
72///     root: V::Element,
73///     state: Proxy<Status>,
74/// }
75///
76/// fn new_widget<V: View>() -> Widget<V> {
77///     let mut state = Proxy::new(Status {
78///         color: "black".to_string(),
79///         message: "Hello".to_string(),
80///     });
81///
82///     // We start out with a `div` element bound to `root`, containing a nested `p` tag
83///     // with the message "Hello" in black.
84///     rsx! {
85///         let root = div() {
86///             p(
87///                 id = "message_wrapper",
88///                 // proxy use in attribute position
89///                 style:color = state(s => &s.color)
90///             ) {
91///                 // proxy use in node position
92///                 {state(s => {
93///                     println!("updating state to: {s:#?}");
94///                     &s.message
95///                 })}
96///             }
97///         }
98///     }
99///
100///     Widget { root, state }
101/// }
102///
103/// println!("creating");
104/// // Verify at creation that the view shows "Hello" in black.
105/// let mut w = new_widget::<mogwai::ssr::Ssr>();
106/// assert_eq!(
107///     r#"<div><p id="message_wrapper" style="color: black;">Hello</p></div>"#,
108///     w.root.html_string()
109/// );
110///
111/// // Then later we change the message to show "Goodbye" in red.
112/// w.state.set(Status {
113///     color: "red".to_string(),
114///     message: "Goodbye".to_string(),
115/// });
116/// assert_eq!(
117///     r#"<div><p id="message_wrapper" style="color: red;">Goodbye</p></div>"#,
118///     w.root.html_string()
119/// );
120/// ```
121///
122/// ## Nesting arbitrary Rust types as nodes using `ViewChild`
123///
124/// You can nest custom Rust types that implement `ViewChild` within the `rsx!` macro:
125///
126/// ```rust
127/// use mogwai::prelude::*;
128///
129/// #[derive(ViewChild)]
130/// struct MyComponent<V: View> {
131///     #[child]
132///     wrapper: V::Element,
133/// }
134///
135/// fn create_view<V: View>() -> V::Element {
136///     rsx! {
137///         let wrapper = div() {
138///             "This is a custom component."
139///         }
140///     }
141///
142///     let component = MyComponent::<V>{ wrapper };
143///
144///     rsx! {
145///         let root = div() {
146///             h1() { "Welcome" }
147///             {component} // Using the custom component within the view
148///         }
149///     }
150///
151///     root
152/// }
153/// ```
154pub fn rsx(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
155    match syn::parse::<tokens::ViewToken>(input) {
156        Ok(view_token) => view_token.into_token_stream(),
157        Err(error) => error.to_compile_error(),
158    }
159    .into()
160}
161
162/// Derives `ViewChild` for a type.
163///
164/// The type must contain a field annotated with `#[child]`.
165///
166/// Deriving `ViewChild` for an arbitrary Rust type allows you to use that type in the
167/// node position of an [`rsx!`] macro.
168///
169/// # Example
170///
171/// ```rust
172/// use mogwai::prelude::*;
173///
174/// #[derive(ViewChild)]
175/// struct MyComponent<V: View> {
176///     #[child]
177///     wrapper: V::Element,
178/// }
179///
180/// fn nest<V: View>(component: &MyComponent<V>) -> V::Element {
181///     rsx! {
182///         let wrapper = div() {
183///             h1(){ "Hello, world!" }
184///             {component} // <- here `component` is added to the view tree
185///         }
186///     }
187///
188///     wrapper
189/// }
190/// ```
191///
192/// In this example, `MyComponent` is a struct that derives `ViewChild`, allowing it to be used
193/// within the `rsx!` macro. The `wrapper` field is annotated with `#[child]`, indicating that it
194/// is the primary child node for the component.
195#[proc_macro_derive(ViewChild, attributes(child))]
196pub fn impl_derive_viewchild(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
197    let input: syn::DeriveInput = syn::parse_macro_input!(input);
198    let ident = input.ident.clone();
199    let (all_ty_params, maybe_view_ty_param) =
200        input
201            .generics
202            .type_params()
203            .fold((vec![], None), |(mut all, mut found), typ| {
204                all.push(typ.ident.clone());
205
206                for bound in typ.bounds.iter() {
207                    if let syn::TypeParamBound::Trait(t) = bound {
208                        if let Some(last) = t.path.segments.last() {
209                            if last.ident == "View" {
210                                found = Some(typ.ident.clone());
211                            }
212                        }
213                    }
214                }
215
216                (all, found)
217            });
218    let view_ty_param = if let Some(p) = maybe_view_ty_param {
219        p
220    } else {
221        return syn::Error::new(
222            input.generics.span(),
223            "Type must contain a type parameter constrained by View",
224        )
225        .into_compile_error()
226        .into();
227    };
228    let generics = input
229        .generics
230        .type_params()
231        .map(|p| {
232            let mut p = p.clone();
233            p.default = None;
234            p
235        })
236        .collect::<Vec<_>>();
237    if let syn::Data::Struct(data) = input.data {
238        let mut output = quote! {};
239        for field in data.fields.iter() {
240            let has_child_annotation = field.attrs.iter().any(|attr| attr.path().is_ident("child"));
241            if has_child_annotation {
242                let field = &field.ident;
243                output = quote! {
244                    impl <#(#generics),*> mogwai::prelude::ViewChild<#view_ty_param> for #ident<#(#all_ty_params),*> {
245                        fn as_append_arg(&self) -> mogwai::prelude::AppendArg<#view_ty_param, impl Iterator<Item = std::borrow::Cow<'_, #view_ty_param::Node>>> {
246                            self.#field.as_append_arg()
247                        }
248                    }
249                };
250                break;
251            }
252        }
253        output
254    } else {
255        quote! { compile_error!("Deriving ViewChild is only supported on struct types") }
256    }
257    .into()
258}
259
260#[cfg(test)]
261mod test {
262    use std::str::FromStr;
263
264    #[test]
265    fn can_parse_rust_closure() {
266        let expr: syn::Expr = syn::parse_str(r#"|i:i32| format!("{}", i)"#).unwrap();
267        match expr {
268            syn::Expr::Closure(_) => {}
269            _ => panic!("wrong expr parse, expected closure"),
270        }
271    }
272
273    #[test]
274    fn can_token_stream_from_string() {
275        let _ts = proc_macro2::TokenStream::from_str(r#"|i:i32| format!("{}", i)"#).unwrap();
276    }
277
278    #[test]
279    fn can_parse_from_token_stream() {
280        let _ts = proc_macro2::TokenStream::from_str(r#"<div class="any_class" />"#).unwrap();
281    }
282
283    #[test]
284    #[allow(dead_code)]
285    fn moggy() {
286        use mogwai::prelude::*;
287
288        #[derive(ViewChild)]
289        struct MyComponent<V: View> {
290            #[child]
291            wrapper: V::Element,
292        }
293
294        fn create_view<V: View>() -> V::Element {
295            rsx! {
296                let wrapper = div() {
297                    "This is a custom component."
298                }
299            }
300            let component = MyComponent::<V> { wrapper };
301
302            rsx! {
303                let root = div() {
304                    h1() { "Welcome" }
305                    {component} // Using the custom component within the view
306                }
307            }
308
309            root
310        }
311    }
312
313    #[test]
314    #[allow(dead_code)]
315    fn nest() {
316        use mogwai::prelude::*;
317
318        #[derive(ViewChild)]
319        struct MyComponent<V: View> {
320            #[child]
321            wrapper: V::Element,
322        }
323
324        fn nest<V: View>(component: &MyComponent<V>) -> V::Element {
325            rsx! {
326                let wrapper = div() {
327                    h1(){ "Hello, world!" }
328                    {component} // <- here `component` is added to the view tree
329                }
330            }
331
332            wrapper
333        }
334    }
335
336    #[test]
337    #[allow(dead_code)]
338    fn nest_with_block() {
339        use mogwai::prelude::*;
340
341        #[derive(ViewChild)]
342        struct MyComponent<V: View> {
343            #[child]
344            wrapper: V::Element,
345            text: V::Text,
346        }
347
348        impl<V: View> MyComponent<V> {
349            fn new() -> Self {
350                rsx! {
351                    let wrapper = p() {
352                        let text = "Here is text"
353                    }
354                }
355                Self { wrapper, text }
356            }
357        }
358
359        fn nest<V: View>() -> V::Element {
360            rsx! {
361                let wrapper = div() {
362                    h1(){ "Hello, world!" }
363                    {{
364                        let component = MyComponent::<V>::new();
365                        component.text.set_text("blarg");
366                        component
367                    }}
368                }
369            }
370
371            wrapper
372        }
373    }
374}