leptos_mview/
lib.rs

1/*!
2An alternative `view!` macro for [Leptos](https://github.com/leptos-rs/leptos/tree/main) inspired by [maud](https://maud.lambda.xyz/).
3
4# Example
5
6A little preview of the syntax:
7
8```
9use leptos::prelude::*;
10use leptos_mview::mview;
11
12#[component]
13fn MyComponent() -> impl IntoView {
14    let (value, set_value) = signal(String::new());
15    let red_input = move || value().len() % 2 == 0;
16
17    mview! {
18        h1.title("A great website")
19        br;
20
21        input
22            type="text"
23            data-index=0
24            class:red={red_input}
25            prop:{value}
26            on:change={move |ev| {
27                set_value(event_target_value(&ev))
28            }};
29
30        Show
31            when=[!value().is_empty()]
32            fallback=[mview! { "..." }]
33        (
34            Await
35                future={fetch_from_db(value())}
36                blocking
37            |db_info| (
38                p("Things found: " strong({*db_info}) "!")
39                p("Is bad: " f["{}", red_input()])
40            )
41        )
42    }
43}
44
45async fn fetch_from_db(data: String) -> usize { data.len() }
46```
47
48<details>
49<summary> Explanation of the example: </summary>
50
51```
52use leptos::prelude::*;
53use leptos_mview::mview;
54
55#[component]
56fn MyComponent() -> impl IntoView {
57    let (value, set_value) = signal(String::new());
58    let red_input = move || value().len() % 2 == 0;
59
60    mview! {
61        // specify tags and attributes, children go in parentheses.
62        // classes (and ids) can be added like CSS selectors.
63        // same as `h1 class="title"`
64        h1.title("A great website")
65        // elements with no children end with a semi-colon
66        br;
67
68        input
69            type="text"
70            data-index=0 // kebab-cased identifiers supported
71            class:red={red_input} // non-literal values must be wrapped in braces
72            prop:{value} // shorthand! same as `prop:value={value}`
73            on:change={move |ev| { // event handlers same as leptos
74                set_value(event_target_value(&ev))
75            }};
76
77        Show
78            // values wrapped in brackets `[body]` are expanded to `{move || body}`
79            when=[!value().is_empty()] // `{move || !value().is_empty()}`
80            fallback=[mview! { "..." }] // `{move || mview! { "..." }}`
81        ( // I recommend placing children like this when attributes are multi-line
82            Await
83                future={fetch_from_db(value())}
84                blocking // expanded to `blocking=true`
85            // children take arguments with a 'closure'
86            // this is very different to `let:db_info` in Leptos!
87            |db_info| (
88                p("Things found: " strong({*db_info}) "!")
89                // bracketed expansion works in children too!
90                // this one also has a special prefix to add `format!` into the expansion!
91                //    {move || format!("{}", red_input()}
92                p("Is bad: " f["{}", red_input()])
93            )
94        )
95    }
96}
97
98// fake async function
99async fn fetch_from_db(data: String) -> usize { data.len() }
100```
101
102</details>
103
104# Purpose
105
106The `view!` macros in Leptos is often the largest part of a component, and can get extremely long when writing complex components. This macro aims to be as **concise** as possible, trying to **minimise unnecessary punctuation/words** and **shorten common patterns**.
107
108# Compatibility
109
110This macro will be compatible with the latest stable release of Leptos. The macro references Leptos items using `::leptos::...`, no items are re-exported from this crate. Therefore, this crate will likely work with any Leptos version if no view-related items are changed.
111
112The below are the versions with which I have tested it to be working. It is likely that the macro works with more versions of Leptos.
113
114| `leptos_mview` version | Compatible `leptos` version |
115| ---------------------- | --------------------------- |
116| `0.1`                  | `0.5`                       |
117| `0.2`                  | `0.5`, `0.6`                |
118| `0.3`                  | `0.6`                       |
119| `0.4`                  | `0.7`                       |
120
121This crate also has a feature `"nightly"` that enables better proc-macro diagnostics (simply enables the nightly feature in proc-macro-error2. Necessary while [this pr](https://github.com/GnomedDev/proc-macro-error-2/pull/5) is not yet merged).
122
123# Syntax details
124
125## Elements
126
127Elements have the following structure:
128
1291. Element / component tag name / path (`div`, `App`, `component::Codeblock`).
1302. Any classes or ids prefixed with a dot `.` or hash `#` respectively.
1313. A space-separated list of attributes and directives (`class="primary"`, `on:click={...}`).
1324. Children in parens or braces (`("hi")` or `{ "hi!" }`), or a semi-colon for no children (`;`).
133
134Example:
135```
136# use leptos_mview::mview; use leptos::prelude::*;
137# let handle_input = |_| ();
138# #[component] fn MyComponent(data: i32, other: &'static str) -> impl IntoView {}
139mview! {
140    div.primary(strong("hello world"))
141    input type="text" on:input={handle_input};
142    MyComponent data=3 other="hi";
143}
144# ;
145```
146
147Adding generics is the same as in Leptos: add it directly after the component name, with or without the turbofish.
148
149```
150# use leptos::prelude::*; use leptos_mview::mview;
151# use core::marker::PhantomData;
152#[component]
153pub fn GenericComponent<S>(ty: PhantomData<S>) -> impl IntoView {
154    std::any::type_name::<S>()
155}
156
157#[component]
158pub fn App() -> impl IntoView {
159    mview! {
160        // both with and without turbofish is supported
161        GenericComponent::<String> ty={PhantomData};
162        GenericComponent<usize> ty={PhantomData};
163        GenericComponent<i32> ty={PhantomData};
164    }
165}
166```
167
168Note that due to [Reserving syntax](https://doc.rust-lang.org/edition-guide/rust-2021/reserving-syntax.html), the `#` for ids must have a space before it.
169
170```
171# use leptos_mview::mview; use leptos::prelude::*;
172mview! {
173    nav #primary ("...")
174    // not allowed: nav#primary ("...")
175}
176# ;
177```
178
179Classes/ids created with the selector syntax can be mixed with the attribute `class="..."` and directive `class:a-class={signal}` as well.
180
181There is also a special element `!DOCTYPE html;`, equivalent to `<!DOCTYPE html>`.
182
183## Slots
184
185[Slots](https://docs.rs/leptos/latest/leptos/attr.slot.html) ([another example](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs)) are supported by prefixing the struct with `slot:` inside the parent's children.
186
187The name of the parameter in the component function must be the same as the slot's name, in snake case.
188
189Using the slots defined by the [`SlotIf` example linked](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs):
190```
191use leptos::prelude::*;
192use leptos_mview::mview;
193
194#[component]
195pub fn App() -> impl IntoView {
196    let (count, set_count) = signal(0);
197    let is_even = Signal::derive(move || count() % 2 == 0);
198    let is_div5 = Signal::derive(move || count() % 5 == 0);
199    let is_div7 = Signal::derive(move || count() % 7 == 0);
200
201    mview! {
202        SlotIf cond={is_even} (
203            slot:Then ("even")
204            slot:ElseIf cond={is_div5} ("divisible by 5")
205            slot:ElseIf cond={is_div7} ("divisible by 7")
206            slot:Fallback ("odd")
207        )
208    }
209}
210# #[slot] struct Then { children: ChildrenFn }
211# #[slot] struct ElseIf { #[prop(into)] cond: Signal<bool>, children: ChildrenFn }
212# #[slot] struct Fallback { children: ChildrenFn }
213#
214# #[component]
215# fn SlotIf(
216#     #[prop(into)] cond: Signal<bool>,
217#     then: Then,
218#     #[prop(optional)] else_if: Vec<ElseIf>,
219#     #[prop(optional)] fallback: Option<Fallback>,
220# ) -> impl IntoView {
221#     move || {
222#         if cond() {
223#             (then.children)().into_any()
224#         } else if let Some(else_if) = else_if.iter().find(|i| (i.cond)()) {
225#             (else_if.children)().into_any()
226#         } else if let Some(fallback) = &fallback {
227#             (fallback.children)().into_any()
228#         } else {
229#             ().into_any()
230#         }
231#     }
232# }
233```
234
235## Values
236
237There are (currently) 3 main types of values you can pass in:
238
239- **Literals** can be passed in directly to attribute values (like `data=3`, `class="main"`, `checked=true`).
240    - However, children do not accept literal numbers or bools - only strings.
241        ```compile_fail
242        # use leptos_mview::mview;
243        // does NOT compile.
244        mview! { p("this works " 0 " times: " true) }
245        # ;
246        ```
247
248- Everything else must be passed in as a **block**, including variables, closures, or expressions.
249    ```
250    # use leptos_mview::mview; use leptos::prelude::*;
251    # let input_type = "text";
252    # let handle_input = |_a: i32| ();
253    mview! {
254        input
255            class="main"
256            checked=true
257            data-index=3
258            type={input_type}
259            on:input={move |_| handle_input(1)};
260    }
261    # ;
262    ```
263
264    This is not valid:
265    ```compile_fail
266    # use leptos_mview::mview;
267    let input_type = "text";
268    // ❌ This is not valid! Wrap input_type in braces.
269    mview! { input type=input_type }
270    # ;
271    ```
272
273- Values wrapped in **brackets** (like `value=[a_bool().to_string()]`) are shortcuts for a block with an empty closure `move || ...` (to `value={move || a_bool().to_string()}`).
274    ```rust
275    # use leptos::prelude::*; use leptos_mview::mview;
276    # let number = || 3;
277    mview! {
278        Show
279            fallback=[()] // common for not wanting a fallback as `|| ()`
280            when=[number() % 2 == 0] // `{move || number() % 2 == 0}`
281        (
282            "number + 1 = " [number() + 1] // works in children too!
283        )
284    }
285    # ;
286    ```
287
288    - Note that this always expands to `move || ...`: for any closures that take an argument, use the full closure block instead.
289        ```compile_error
290        # use leptos_mview::mview;
291        # use leptos::logging::log;
292        mview! {
293            input type="text" on:click=[log!("THIS DOESNT WORK")];
294        }
295        ```
296
297        Instead:
298        ```
299        # use leptos_mview::mview; use leptos::prelude::*;
300        # use leptos::logging::log;
301        mview! {
302            input type="text" on:click={|_| log!("THIS WORKS!")};
303        }
304        # ;
305        ```
306
307The bracketed values can also have some special prefixes for even more common shortcuts!
308- Currently, the only one is `f` - e.g. `f["{:.2}", stuff()]`. Adding an `f` will add `format!` into the closure. This is equivalent to `[format!("{:.2}", stuff())]` or `{move || format!("{:.2}", stuff())}`.
309
310## Attributes
311
312### Key-value attributes
313
314Most attributes are `key=value` pairs. The `value` follows the rules from above. The `key` has a few variations:
315
316- Standard identifier: identifiers like `type`, `an_attribute`, `class`, `id` etc are valid keys.
317- Kebab-case identifier: identifiers can be kebab-cased, like `data-value`, `an-attribute`.
318    - NOTE: on HTML elements, this will be put on the element as is: `div data-index="0";` becomes `<div data-index="0"></div>`. **On components**, hyphens are converted to underscores then passed into the component builder.
319
320        For example, this component:
321        ```ignore
322        #[component]
323        fn Something(some_attribute: i32) -> impl IntoView { ... }
324        ```
325
326        Can be used elsewhere like this:
327        ```
328        # use leptos::prelude::*; use leptos_mview::mview;
329        # #[component] fn Something(some_attribute: i32) -> impl IntoView {}
330        mview! { Something some-attribute=5; }
331        # ;
332        ```
333
334        And the `some-attribute` will be passed in to the `some_attribute` argument.
335
336- Attribute shorthand: if the name of the attribute and value are the same, e.g. `class={class}`, you can replace this with `{class}` to mean the same thing.
337    ```
338    # use leptos_mview::mview; use leptos::prelude::*;
339    let class = "these are classes";
340    let id = "primary";
341    mview! {
342        div {class} {id} ("this has 3 classes and id='primary'")
343    }
344    # ;
345    ```
346
347    See also: [kebab-case identifiers with attribute shorthand](#kebab-case-identifiers-with-attribute-shorthand)
348
349Note that the special `node_ref` or `ref` or `_ref` or `ref_` attribute in Leptos to bind the element to a variable is just `ref={variable}` in here.
350
351### Boolean attributes
352
353Another shortcut is that boolean attributes can be written without adding `=true`. Watch out though! `checked` is **very different** to `{checked}`.
354```
355# use leptos::prelude::*; use leptos_mview::mview;
356// recommend usually adding #[prop(optional)] to all these
357#[component]
358fn LotsOfFlags(wide: bool, tall: bool, red: bool, curvy: bool, count: i32) -> impl IntoView {}
359
360mview! { LotsOfFlags wide tall red=false curvy count=3; }
361# ;
362// same as...
363mview! { LotsOfFlags wide=true tall=true red=false curvy=true count=3; }
364# ;
365```
366
367See also: [boolean attributes on HTML elements](#boolean-attributes-on-html-elements)
368
369### Directives
370
371Some special attributes (distinguished by the `:`) called **directives** have special functionality. All have the same behaviour as Leptos. These include:
372- `class:class-name=[when to show]`
373- `style:style-key=[style value]`
374- `on:event={move |ev| event handler}`
375- `prop:property-name={signal}`
376- `attr:name={value}`
377- `clone:ident_to_clone`
378- `use:directive_name` or `use:directive_name={params}`
379- `bind:checked={rwsignal}` or `bind:value={(getter, setter)}`
380
381All of these directives except `clone` also support the attribute shorthand:
382
383```
384# use leptos::prelude::*; use leptos_mview::mview;
385let color = RwSignal::new("red".to_string());
386let disabled = false;
387mview! {
388    div style:{color} class:{disabled};
389}
390# ;
391```
392
393The `class` and `style` directives also support using string literals, for more complicated names. Make sure the string for `class:` doesn't have spaces, or it will panic!
394
395```
396# use leptos::prelude::*; use leptos_mview::mview;
397let yes = move || true;
398mview! {
399    div class:"complex-[class]-name"={yes}
400        style:"doesn't-exist"="white";
401}
402# ;
403```
404
405Note that the `use:` directive automatically calls `.into()` on its argument, consistent with behaviour from Leptos.
406
407## Children
408
409You may have noticed that the `let:data` prop was missing from the previous section on directive attributes!
410
411This is replaced with a closure right before the children block. This way, you can pass in multiple arguments to the children more easily.
412
413```
414# use leptos::prelude::*; use leptos_mview::mview;
415# leptos::task::Executor::init_futures_executor().unwrap();
416mview! {
417    Await
418        future={async { 3 }}
419    |monkeys| (
420        p({*monkeys} " little monkeys, jumping on the bed.")
421    )
422}
423# ;
424```
425
426Note that you will usually need to add a `*` before the data you are using. If you forget that, rust-analyser will tell you to dereference here: `*{monkeys}`. This is obviously invalid - put it inside the braces.
427
428Children can be wrapped in either braces or parentheses, whichever you prefer.
429
430```
431# use leptos::prelude::*; use leptos_mview::mview;
432mview! {
433    p {
434        "my " strong("bold") " and " em("fancy") " text."
435    }
436}
437# ;
438```
439
440Summary from the previous section on values in case you missed it: children can be literal strings (not bools or numbers!), blocks with Rust code inside (`{*monkeys}`), or the closure shorthand `[number() + 1]`.
441
442Children with closures are also supported on slots.
443
444# Extra details
445
446## Kebab-case identifiers with attribute shorthand
447
448If an attribute shorthand has hyphens:
449- On components, both the key and value will be converted to underscores.
450    ```
451    # use leptos::prelude::*; use leptos_mview::mview;
452    # #[component] fn Something(some_attribute: i32) -> impl IntoView {}
453    let some_attribute = 5;
454    mview! { Something {some-attribute}; }
455    # ;
456    // same as...
457    mview! { Something {some_attribute}; }
458    # ;
459    // same as...
460    mview! { Something some_attribute={some_attribute}; }
461    # ;
462    ```
463
464- On HTML elements, the key will keep hyphens, but the value will be turned into an identifier with underscores.
465    ```
466    # use leptos_mview::mview; use leptos::prelude::*;
467    let aria_label = "a good label";
468    mview! { input {aria-label}; }
469    # ;
470    // same as...
471    mview! { input aria-label={aria_label}; }
472    # ;
473    ```
474
475## Boolean attributes on HTML elements
476
477Note the behaviour from Leptos: setting an HTML attribute to true adds the attribute with no value associated.
478```
479# use leptos::prelude::*;
480view! { <input type="checkbox" checked=true data-smth=true not-here=false /> }
481# ;
482```
483Becomes `<input type="checkbox" checked data-smth />`, NOT `checked="true"` or `data-smth="true"` or `not-here="false"`.
484
485To have the attribute have a value of the string "true" or "false", use `.to_string()` on the bool. Make sure that it's in a closure if you're working with signals too.
486```
487# use leptos::prelude::*;
488# use leptos_mview::mview;
489let boolean_signal = RwSignal::new(true);
490mview! { input type="checkbox" checked=[boolean_signal().to_string()]; }
491# ;
492// or, if you prefer
493mview! { input type="checkbox" checked=f["{}", boolean_signal()]; }
494# ;
495```
496
497# Contributing
498
499Please feel free to make a PR/issue if you have feature ideas/bugs to report/feedback :)
500
501 */
502
503// note: to transfer above to README.md, install `cargo-rdme` and run
504// `cargo rdme`
505// Some bits are slightly broken, fix up stray `compile_error`/
506// `ignore`, missing `rust` annotations and remove `#` lines.
507
508pub use leptos_mview_macro::mview;
509
510/// Not for public use. Do not implement anything on this.
511#[doc(hidden)]
512pub struct MissingValueAfterEq;