Finally, here's the release for version 0.1.0
. Thanks to everyone who's been working with the alpha, beta, and bleeding-edge versions over the last few weeks to help get everything ready.
Most of the release notes below are copied from the 0.1.0-alpha
release, but include a few updates. All these changes are relative to 0.0.22
.
Features
More flexible rendering
You can now easily mix and match different types of node in a Fragment
, which previously needed to be a Vec
of all the same types (like a Vec<Fragment>
).
view! { cx,
<>
"This is some text."
<p>"Here's an element."</p>
<ComponentA/>
</>
}
In fact, the <>
fragment itself is optional.
view! { cx,
"This is some text."
<p>"Here's an element."</p>
<ComponentA/>
}
Typed HTML elements
HTML elements generated by the view!
macro are now typed, and implement Deref
to give a correctly-typed web_sys
element.
// returns `HtmlElement<Input>`
// derefs to a `web_sys::HtmlInputElement`
view! { cx, <input type="text"/> }
store_value
You can now store non-reactive values in the reactive system to take advantage of the same Copy + 'static
reference system without the overhead or abstraction leak of storing something in a signal. This is useful for objects you need to reference in multiple places throughout your application without cloning.
// this structure is neither `Copy` nor `Clone`
pub struct MyUncloneableData {
pub value: String
}
// ✅ you can move the `StoredValue` into closures and access it with .with()
let data = store_value(cx, MyUncloneableData { value: "a".into() });
let callback_a = move || data.with(|data| data.value == "a");
let callback_b = move || data.with(|data| data.value == "b");
Signal<T>
and SignalSetter<T>
abstractions
These wrappers allow you to define APIs that can take any kind of signal. For example, Signal<T>
can be a ReadSignal
, RwSignal
, Memo
, or derived signal. This is especially useful when defining components interfaces. (There's even a MaybeSignal
for types that may just be T
or may be a Signal<T>
.)
let (count, set_count) = create_signal(cx, 2);
let double_count = Signal::derive(cx, move || count() * 2);
let memoized_double_count = create_memo(cx, move |_| count() * 2);
// this function takes any kind of wrapped signal
fn above_3(arg: &Signal<i32>) -> bool {
// ✅ calling the signal clones and returns the value
// it is a shorthand for arg.get()
arg() > 3
}
assert_eq!(above_3(&count.into()), false);
assert_eq!(above_3(&double_count), true);
assert_eq!(above_3(&memoized_double_count.into()), true);
Massively improved Rust Analyzer support
DX with this new system is much better. You'll see hugely improved language-server integration for the view!
macro, including features like doc comments for HTML elements, autocompletion for component properties, clicking to go to the definition of a component, type hints for component arguments, etc.
Inline documentation in the #[component]
macro
You can now document components and their properties directly in the #[component]
macro like this:
/// A simple counter component.
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
cx: Scope,
/// The starting value for the counter
#[prop(optional)]
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32
) -> impl IntoView {
// ...
}
The macro will automatically generate docs for the components and for each of its props.
#[prop]
attributes for component props
You can use the #[prop]
attribute macro to mark component props in several ways:
#[prop(into)]
: This will call.into()
on any value passed into the component prop. (For example,
you could apply#[prop(into)]
to a prop that takes Signal, which would
allow users to pass a ReadSignal or RwSignal
and automatically convert it.)#[prop(optional)]
: If the user does not specify this property when they use the component,
it will be set to its default value. If the property type isOption<T>
, values should be passed
asname=T
and will be received asSome(T)
.#[prop(optional_no_strip)]
: The same asoptional
, but requires values to be passed asNone
or
Some(T)
explicitly. This means that the optional property can be omitted (and beNone
), or explicitly
specified as eitherNone
orSome(T)
.#[prop(default = X)]
: The property is optional, and will be set to the default value provided if it is not passed.
tracing
support
We've added support for debugging using the tracing
crate, integrated into both the renderer and the reactive system. This means that when an effect or an event listener fires, you can use trace!(...)
to get an insight into the flow of your program. See leptos_dom/examples/test-bench
for an example setup; a separate crate to make this tracing setup easier will be forthcoming.
cargo-leptos
integration improvements
cargo-leptos 0.1.2
and the Actix/Axum integrations in 0.1.0
now work together seamlessly, allowing you to create full-stack applications with as little boilerplate as possible.
And much more!
- Comments that mark component boundaries for easier debugging, when in debug mode
- Removing the need to wrap things that
<Router/>
in a<div>
for no obvious reason - builder API that allows you to crate a UI without using the
view
macro - and all sorts of things I'm forgetting!
Breaking Changes
Here are a few things to watch out for as you migrate:
- If you're using
cargo-leptos
already and run into any issues (for example, with the path of a JS or Wasm file), be sure to check out the latest examples and the starter. The extra structures here have changed a bit over time as we've tried to figure out the best patterns. Cloning the starter and adding your existing application code should work fine. - All component functions should now return
-> impl IntoView
- The
<For/>
component now takes aview
property, instead of you passing the view function as the child of the component - Likewise, the
<Route/>
component takes aview
property instead ofelement
for consistency children
properties are now typedBox<dyn Fn(Scope) -> Fragment>
- The
view!
macro returns distinct structures, which are all different types. In other words, this code no longer compiles
// this won't work
if some_condition {
view! { cx, <p>"P if true"</p> }
} else {
view! { cx, <span>"SPAN if false"</span> }
}
because the first branch now returns HtmlElement<P>
while the second returns HtmlElement<Span>
.
In this case, simply add .into_any()
to each to return HtmlElement<AnyElement>
from both. In general, you can always call .into_view(cx)
to return a wrapping View
type instead.
// this works
if some_condition {
view! { cx, <p>"P if true"</p> }.into_any()
} else if something_else {
view! { cx, <span>"SPAN if false"</span> }.into_any()
}