No Macros: The View Builder Syntax
If you’re perfectly happy with the
view!
macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited rustfmt
support. (Although, you should check out leptosfmt
, which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the view
macro provides.
If you fall into any of those camps, the builder syntax may be for you.
The view
macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the view
macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice!
First off, if you want you can even drop the #[component]
macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
Elements are created by calling a function with the same name as the HTML element:
p()
You can add children to the element with .child()
, which takes a single child or a tuple or array of types that implement IntoView
.
p().child((em().child("Big, "), strong().child("bold "), "text"))
Attributes are added with .attr()
. This can take any of the same types that you could pass as an attribute into the view macro (types that implement Attribute
).
p().attr("id", "foo")
.attr("data-count", move || count.get().to_string())
They can also be added with attribute methods, which are available for any built-in HTML attribute name:
p().id("foo")
.attr("data-count", move || count.get().to_string())
Similarly, the class:
, prop:
, and style:
syntaxes map directly onto .class()
, .prop()
, and .style()
methods.
Event listeners can be added with .on()
. Typed events found in leptos::ev
prevent typos in event names and allow for correct type inference in the callback function.
button()
.on(ev::click, move |_| set_count.set(0))
.child("Clear")
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: i32) -> impl IntoView {
let (count, set_count) = signal(initial_value);
div().child((
button()
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.set(0))
.child("Clear"),
button()
.on(ev::click, move |_| *set_count.write() -= step)
.child("-1"),
span().child(("Value: ", move || count.get(), "!")),
button()
.on(ev::click, move |_| *set_count.write() += step)
.child("+1"),
))
}
Using Components with the Builder Syntax
To create your own components with the builder syntax, you can simply use plain functions (see above). To use other components (for example, the built-in For
or Show
control-flow components), you can take advantage of the fact that each component is a function of one component props argument, and component props have their own builder.
You can either use the component props builder:
use leptos::html::p;
let (value, set_value) = signal(0);
Show(
ShowProps::builder()
.when(move || value.get() > 5)
.fallback(|| p().child("I will appear if `value` is 5 or lower"))
.children(ToChildren::to_children(|| {
p().child("I will appear if `value` is above 5")
}))
.build(),
)
or you can directly build the props struct:
use leptos::html::p;
let (value, set_value) = signal(0);
Show(ShowProps {
when: move || value.get() > 5,
fallback: (|| p().child("I will appear if `value` is 5 or lower")).into(),
children: ToChildren::to_children(|| p().child("I will appear if `value` is above 5")),
})
Using the component builder correctly applies the various modifiers like #[prop(into)]
; using the struct syntax, we’ve applied this manually by calling .into()
ourselves.
Expanding Macros
Not every feature of the view
macro or component
macro syntax has been described here in detail. However, Rust provides you the tools you need to understand what is going on with any macro. Specifically, rust-analyzer’s "expand macro recursively" feature allows you to expand any macro to show the code it generates, and cargo-expand
expands all the macros in a project into regular Rust code. The rest of this book will continue using the view
macro syntax, but if you’re ever unsure how to translate this into the builder syntax, you can use these tools to explore the code that is generated.