Releases: maciejhirsz/kobold
0.9.1
0.9
⚠️ BREAKING:Hook::bind
(used internally by thebind!
macro) now returns a concrete typeBound<_, _>
instead of animpl Listener<_>
. While this is a breaking change, upgrading from 0.8 shouldn't require any changes for most projects.- Bound listeners are now
Copy
if the closure they're created from is alsoCopy
.bind! { state: let reset = |_| *state = 0; } view! { // `reset` previously would be typed `impl Listener<_>`which made it `!Copy` <input onclick={reset} onchange={reset}> }
- Added a new
kobold::diff::VString
type: a versioned string drop-in replacement for a regularString
. This type provides all the regular methods of aString
viaDeref
andDerefMut
traits, but it tracks mutable access by incrementing an internal version counter. This makes diffing&VString
much faster and cheaper than&String
or even&str
in places where theref
keyword cannot be used. - Added
serde
feature flag which implementsSerialize
andDeserialize
traits fromserde
forVString
.
0.8.1
0.8
This release is mostly about internal optimizations, read the related blog post if you are interested in the details.
- Event listeners no longer allocate any memory. (#78)
ListProduct
is now using a new internal collection to allocate the generated products. (#78)- State-bound zero-sized event listeners have their update method turned into a no-op. (#75)
- Event listeners no longer store their
JsValue
in their product, making them more memory efficient. (#75) - Added
rlsf
feature to quickly enable and turn on the rlsf allocator. (#78)
While the above changes are mostly to do with internals and the user-facing API surface hasn't changed, to make the new event listeners work the following is now a required addition in the Trunk.toml
configuration:
[build]
pattern_script = "<script type=\"module\">import init, { koboldCallback } from '{base}{js}';init('{base}{wasm}');window.koboldCallback = koboldCallback;</script>"
Debug builds will check if koboldCallback
is defined and log an error in console with this exact line in it.
This will hopefully not be required in the future, but it requires upstream changes in Trunk or wasm-bindgen to allow for exported Rust functions to be invoked from imported JavaScript modules without knowing the name of the binary that's being compiled.
0.7.1
0.7
Optional parameters
It's now possible to annotate parameters inside the #[component]
attribute macro to be optional and use a default value if omitted:
#[component(
// Make `name` an optional parameter, defaults to `"Kobold"`
name?: "Kobold",
// Make `age` an optional parameter, use the `Default` value
age?,
)]
fn Greeter<'a>(name: &'a str, age: Option<u32>) -> impl View + 'a {
let age = age.map(|age| view!(", you are "{ age }" years old"));
view! {
<p> "Hello "{ name }{ age }
}
}
view! {
// Hello Kobold
<Greeter />
// Hello Alice
<Greeter name="Alice" />
// Hello Bob, you are 42 years old
<Greeter name="Bob" age={42} />
}
Setting an optional parameter requires that the value implements the new Maybe
trait. Because Maybe<T>
is implemented for Option<T>
we can set age
to plain 42
without wrapping it in Some
in the example above.
The reverse is also true which allows for optional parameters of any type to be set using an Option
:
#[component(code?: 200)]
fn StatusCode(code: u32) -> impl View {
view! {
<p> "Status code was "{ code }
}
}
view! {
// Status code was 200
<StatusCode />
// Status code was 404
<StatusCode code={404} />
// Status code was 200
<StatusCode code={None} />
// Status code was 500
<StatusCode code={Some(500)} />
}
This is a breaking change when it comes to internals (the interaction between #[component]
and view!
macros), as components now set their fields using methods in a builder pattern. The user facing API for components as documented in 0.6 however remains compatible.
Optional parameters are a zero-cost feature, meaning if you are not using them your components will be just as fast and your compiled Wasm binary will be just as small as before (smaller actually, more on that later).
Optional closing tags for HTML elements
💡 Note: Following changes are all backwards compatible with the JSX-esque syntax view!
macro has been using thus far.
End of macro closes all tags:
view! {
// no closing tags necessary at the end of macro
<header><h1> "Hello Kobold"
}
Closing an ancestor closes all children:
view! {
<div>
<header>
<h1> "Hello Kobold"
// Closing the `div` closes both `h1` and `header`
</div>
}
view! {
// trailing `/` is mandatory for components without children
<MyComponent />
<p> "Paragraph under the component"
}
Void elements (img
, input
, etc.) have no closing tags
view! {
<div>
"This text is inside the div"
// `input` is forbidden from having children and doesn't need closing
<input type="text">
"This text is also inside the div"
}
Implicitly closing tags (li
, td
, etc.)
Some tags implicitly close other open tags as per the HTML spec, making the following legal Kobold syntax:
view! {
<ul.my-list>
// `li` closes previous `li`
<li> "Item 1"
<li> "Item 2"
<li> "Item 3"
}
view! {
<table.some-class>
<tr>
// `td` closes previous `td` or `th`
<td> "Row 1, Col 1"
<td> "Row 1, Col 2"
<td> "Row 1, Col 3"
// `tr` closes previous `td`, `th`, and/or `tr`
<tr>
<td> "Row 2, Col 1"
<td> "Row 2, Col 2"
<td> "Row 2, Col 3"
}
Async event handlers
The Hook::signal
method has been removed, as it lead to situations where you could easily shoot yourself in the foot (#56):
stateful(0, |count| {
let singal = count.signal();
// Attempting to update state inside the view always failed
// as state is currently borrowed
signal.update(|count| *count += 1);
count.get()
})
Since the only reason you'd want to have a Signal
inside the view was to create an async
even handler, Hook<T>
now has a bind_async
method that provides an owned Singal<T>
(as opposed to &mut T
in sync bind
) and handles returned future.
See the csv_editor
example:
let onload = state.bind_async(|state, event: Event<InputElement>| async move {
let file = match event.target().files().and_then(|list| list.get(0)) {
Some(file) => file,
None => return,
};
state.update(|state| state.name = file.name());
if let Ok(table) = csv::read_file(file).await {
state.update(move |state| state.table = table);
}
});
The bind!
macro currently only handles synchronous binds. This is likely to change in the next release but will require a rewrite of the macro into a procedural one.
💡 Note: You can still use the Stateful::once
method to get an access to an owned Signal
outside of the view and without invoking an event handler, see the interval
example:
#[component]
fn Elapsed(seconds: u32) -> impl View {
stateful(seconds, |seconds| {
bind! {
seconds:
let onclick = move |_| *seconds = 0;
}
view! {
<p>
"Elapsed seconds: "{ seconds }" "
// `{onclick}` here is shorthand for `onclick={onclick}`
<button {onclick}>"Reset"</button>
}
})
.once(|signal| {
// `signal` is an owned `Signal<u32>` and can be safely moved.
//
// `Interval` is returned here and will be safely dropped with the component.
Interval::new(1000, move || {
signal.update(|seconds| *seconds += 1);
})
})
}
Type hints
The view!
macro now provides type hints for all attributes in rust-analyzer, including those that are compiled away to raw JavaScript:
Internals
A lot of internals have changed, notable PRs: #63, #64, #46.
The Wasm blob in the TodoMVC example is now down to 16.98kb gzipped (was 17.16kb in 0.6 and 18.08kb in 0.5)
0.6
License change to MPL
Mandatory disclaimer: I am not a lawyer and this is not legal advice.
After some discussion this release has switched from LGPL-3.0 to the Mozilla Public License version 2.0. LGPL is the GNU License intended for libraries that's not as restrictive as the actual GPL, however its wording makes it problematic to use when a library is statically linked as commonly happens in Rust.
In practice the switch to MPL-2.0 means that you can freely use Kobold in projects that themselves use different licenses, including closed-source proprietary projects, as long as any changes to Kobold itself are made open-source under MPL-2.0.
Keywords
The big breaking1 change in this release is the introduction of keywords for { expressions }
in the view!
macro. The keyword is always the first token inside the curly braces, currently 4 of them are defined:
for
makes an iterator into aView
. Replaces theListIteratorExt::list
method.ref
turns on diffing by reference (rather than value) for strings. Replaces theStrExt::fast_diff
method.static
disables diffing and prevents updates to the DOM after initial render of the value. Replaces theStringify::no_diff
method.use
disables diffing and eagerly sets the value in the DOM on every render. This is a new functionality mostly intended to work with thefence
function.
All keywords are defined as raw-identifier functions in the new kobold::keywords
module (e.g.: the static
keyword maps to kobold::keywords::r#static
) for discoverability. In addition to the documentation in the module itself, they are properly spanned and interact nicely with rust-analyzer:
The fence
function
This release adds the kobold::diff::fence
function that guards some inner View
-producing closure against renders unless its guard
value has changed. Signature:
pub const fn fence<D, V, F>(guard: D, render: F) -> Fence<D, F>
where
D: Diff,
V: View,
F: FnOnce() -> V;
Example:
Fencing a view can be a great optimization, especially when combined with the previously mentioned use
keyword:
use kobold::prelude::*;
use kobold::diff::fence;
struct User {
id: usize,
name: String,
email: String,
}
#[component]
fn UserRow(user: &User) -> impl View + '_ {
fence(user.id, || view! {
// This row is only re-rendered if `user.id` has changed
<tr>
<td>{ user.id }</td>
// Assuming that `name` and `email` are always different
// for different users, we can disable diffing here.
<td>{ use &user.name }</td>
<td>{ use &user.email }</td>
</tr>
})
}
Note: while the use
keyword is always safe (it won't cause UI bugs), the performance trade-off only makes sense when used with another mechanism that prevents superfluous renders, such as fence
here.
Internals
There has been a substantial refactoring done to the internals of Kobold, few things have moved around and the way attributes are rendered and how elements are mounted has changed. These are mostly implementation details.
In practice everything is more type-safe and extendable while reducing the size of the produced Wasm file even further. The gzipped Wasm file of the TodoMVC example is now down to 17.16kb (was 18.08kb).
Footnotes
-
The old methods added by extension traits are still supported with deprecation warnings and will be removed in the 0.7 release. ↩
0.5
First publicly announced release! 🎉
Detailed description at https://maciej.codes/2023-03-23-kobold.html