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;