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}