An alternative view! macro for Leptos inspired by maud.
A little preview of the syntax:
use leptos::prelude::*;
use leptos_mview::mview;
#[component]
fn MyComponent() -> impl IntoView {
let (value, set_value) = signal(String::new());
let red_input = move || value().len() % 2 == 0;
mview! {
h1.title("A great website")
br;
input
type="text"
data-index=0
class:red={red_input}
prop:{value}
on:change={move |ev| {
set_value(event_target_value(&ev))
}};
Show
when=[!value().is_empty()]
fallback=[mview! { "..." }]
(
Await
future={fetch_from_db(value())}
blocking
|db_info| (
p("Things found: " strong({*db_info}) "!")
p("Is bad: " f["{}", red_input()])
)
)
}
}
async fn fetch_from_db(data: String) -> usize { data.len() }
Explanation of the example:
use leptos::prelude::*;
use leptos_mview::mview;
#[component]
fn MyComponent() -> impl IntoView {
let (value, set_value) = signal(String::new());
let red_input = move || value().len() % 2 == 0;
mview! {
// specify tags and attributes, children go in parentheses.
// classes (and ids) can be added like CSS selectors.
// same as `h1 class="title"`
h1.title("A great website")
// elements with no children end with a semi-colon
br;
input
type="text"
data-index=0 // kebab-cased identifiers supported
class:red={red_input} // non-literal values must be wrapped in braces
prop:{value} // shorthand! same as `prop:value={value}`
on:change={move |ev| { // event handlers same as leptos
set_value(event_target_value(&ev))
}};
Show
// values wrapped in brackets `[body]` are expanded to `{move || body}`
when=[!value().is_empty()] // `{move || !value().is_empty()}`
fallback=[mview! { "..." }] // `{move || mview! { "..." }}`
( // I recommend placing children like this when attributes are multi-line
Await
future={fetch_from_db(value())}
blocking // expanded to `blocking=true`
// children take arguments with a 'closure'
// this is very different to `let:db_info` in Leptos!
|db_info| (
p("Things found: " strong({*db_info}) "!")
// bracketed expansion works in children too!
// this one also has a special prefix to add `format!` into the expansion!
// {move || format!("{}", red_input()}
p("Is bad: " f["{}", red_input()])
)
)
}
}
// fake async function
async fn fetch_from_db(data: String) -> usize { data.len() }
The 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.
This macro aims to provide well-spanned error messages (so that the whole macro isn't red when you make a mistake). If you find an error message that is extremely confusing or making the entire macro invocation show the error, I consider this a bug! Please report them (but see below first)!
mview! is also much faster than view! when providing autocomplete due to being lazier with parsing. (This hasn't been benchmarked properly, just from my personal experience using this with rust-analyzer. Maybe I'll benchmark this someday.)
Leptos 0.7+ uses a statically typed view tree, which means that an error in one section will often propagate everywhere due to unsatisfied trait bounds / types. Enabling the erase_components cfg flag improves some error messages.
This cfg flag needs to be enabled in either the rust-analyzer config or in .cargo/config.toml. For example, rust-analyzer can be configured in VSCode by adding this to your settings.json:
"rust-analyzer.server.extraEnv": {
"RUSTFLAGS": "--cfg=erase_components"
}
This crate also has a feature "nightly" that enables better diagnostic messages, using the nightly Diagnostic API. (This simply enables the nightly feature in proc-macro-error2. Necessary while this pr is not yet merged.)
This 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.
The 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.
leptos_mview version |
Compatible leptos version |
|---|---|
0.1 |
0.5 |
0.2 |
0.5, 0.6 |
0.3 |
0.6 |
0.4 |
0.7 |
0.5 |
0.8 |
Elements have the following structure:
div, App, component::Codeblock).. or hash # respectively.class="primary", on:click={...}).("hi") or { "hi!" }), or a semi-colon for no children (;).Example:
mview! {
div.primary(strong("hello world"))
input type="text" on:input={handle_input};
MyComponent data=3 other="hi";
}
Adding generics is the same as in Leptos: add it directly after the component name, with or without the turbofish.
#[component]
pub fn GenericComponent<S>(ty: PhantomData<S>) -> impl IntoView {
std::any::type_name::<S>()
}
#[component]
pub fn App() -> impl IntoView {
mview! {
// both with and without turbofish is supported
GenericComponent::<String> ty={PhantomData};
GenericComponent<usize> ty={PhantomData};
GenericComponent<i32> ty={PhantomData};
}
}
Note that due to Reserving syntax, the # for ids must have a space before it.
mview! {
nav #primary ("...")
// not allowed: nav#primary ("...")
}
Classes/ids created with the selector syntax can be mixed with the attribute class="..." and directive class:a-class={signal} as well.
There is also a special element !DOCTYPE html;, equivalent to <!DOCTYPE html>.
Slots (another example) are supported by prefixing the struct with slot: inside the parent's children.
The name of the parameter in the component function must be the same as the slot's name, in snake case.
Using the slots defined by the SlotIf example linked:
use leptos::prelude::*;
use leptos_mview::mview;
#[component]
pub fn App() -> impl IntoView {
let (count, set_count) = signal(0);
let is_even = Signal::derive(move || count() % 2 == 0);
let is_div5 = Signal::derive(move || count() % 5 == 0);
let is_div7 = Signal::derive(move || count() % 7 == 0);
mview! {
SlotIf cond={is_even} (
slot:Then ("even")
slot:ElseIf cond={is_div5} ("divisible by 5")
slot:ElseIf cond={is_div7} ("divisible by 7")
slot:Fallback ("odd")
)
}
}
There are (currently) 3 main types of values you can pass in:
Literals can be passed in directly to attribute values (like data=3, class="main", checked=true).
rust
// does NOT compile.
mview! { p("this works " 0 " times: " true) }Everything else must be passed in as a block, including variables, closures, or expressions.
rust
mview! {
input
class="main"
checked=true
data-index=3
type={input_type}
on:input={move |_| handle_input(1)};
}
This is not valid:
rust
let input_type = "text";
// ❌ This is not valid! Wrap input_type in braces.
mview! { input type=input_type }
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()}).
rust
mview! {
Show
fallback=[()] // common for not wanting a fallback as `|| ()`
when=[number() % 2 == 0] // `{move || number() % 2 == 0}`
(
"number + 1 = " [number() + 1] // works in children too!
)
}
Note that this always expands to move || ...: for any closures that take an argument, use the full closure block instead.
rust
mview! {
input type="text" on:click=[log!("THIS DOESNT WORK")];
}
Instead:
rust
mview! {
input type="text" on:click={|_| log!("THIS WORKS!")};
}
The bracketed values can also have some special prefixes for even more common shortcuts!
- 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())}.
Most attributes are key=value pairs. The value follows the rules from above. The key has a few variations:
type, an_attribute, class, id etc are valid keys.data-value, an-attribute.div data-index="0"; becomes ``. On components, hyphens are converted to underscores then passed into the component builder.
For example, this component:
```rust
#[component]
fn Something(some_attribute: i32) -> impl IntoView { ... }
```
Can be used elsewhere like this:
```rust
mview! { Something some-attribute=5; }
```
And the `some-attribute` will be passed in to the `some_attribute` argument.
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.
rust
let class = "these are classes";
let id = "primary";
mview! {
div {class} {id} ("this has 3 classes and id='primary'")
}
Note 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.
Another shortcut is that boolean attributes can be written without adding =true. Watch out though! checked is very different to {checked}.
// recommend usually adding #[prop(optional)] to all these
#[component]
fn LotsOfFlags(wide: bool, tall: bool, red: bool, curvy: bool, count: i32) -> impl IntoView {}
mview! { LotsOfFlags wide tall red=false curvy count=3; }
// same as...
mview! { LotsOfFlags wide=true tall=true red=false curvy=true count=3; }
See also: boolean attributes on HTML elements
Some special attributes (distinguished by the :) called directives have special functionality. All have the same behaviour as Leptos. These include:
- class:class-name=[when to show]
- style:style-key=[style value]
- on:event={move |ev| event handler}
- prop:property-name={signal}
- attr:name={value}
- clone:ident_to_clone
- use:directive_name or use:directive_name={params}
- bind:checked={rwsignal} or bind:value={(getter, setter)}
All of these directives except clone also support the attribute shorthand:
let color = RwSignal::new("red".to_string());
let disabled = false;
mview! {
div style:{color} class:{disabled};
}
The 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!
let yes = move || true;
mview! {
div class:"complex-[class]-name"={yes}
style:"doesn't-exist"="white";
}
Note that the use: directive automatically calls .into() on its argument, consistent with behaviour from Leptos.
You may have noticed that the let:data prop was missing from the previous section on directive attributes!
This is replaced with a closure right before the children block. This way, you can pass in multiple arguments to the children more easily.
```rust mview! { Await future={async { 3 }} |monkeys| ( p
$ claude mcp add leptos-mview \
-- python -m otcore.mcp_server <graph>