Skip to content

Releases: maciejhirsz/kobold

0.9.1

26 May 09:47
6ffab77
Compare
Choose a tag to compare
  • VString now implements the following traits: Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Write.

0.9

26 May 09:24
3178c41
Compare
Choose a tag to compare
0.9
  • ⚠️ BREAKING: Hook::bind (used internally by the bind! macro) now returns a concrete type Bound<_, _> instead of an impl 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 also Copy.
    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 regular String. This type provides all the regular methods of a String via Deref and DerefMut 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 the ref keyword cannot be used.
  • Added serde feature flag which implements Serialize and Deserialize traits from serde for VString.

0.8.1

30 Apr 17:58
bb6c23c
Compare
Choose a tag to compare
  • Fixed a panic in debug mode when using Signal::update. (#82)

0.8

28 Apr 08:13
1bd25ca
Compare
Choose a tag to compare
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)

⚠️ BREAKING

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

18 Apr 10:02
Compare
Choose a tag to compare

Support attributes with dashes

Added support for DOM attributes with dashes in names, such as data-foo or aria-foo.

Attributes with dashes have their hints spanned to the last identifier:
image

0.7

14 Apr 11:19
c465c03
Compare
Choose a tag to compare
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>
}

⚠️ Note: components are treated as foreign elements and still need to always be closed if they have no children:

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:

image

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

28 Mar 13:00
bd39ad5
Compare
Choose a tag to compare
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 a View. Replaces the ListIteratorExt::list method.
  • ref turns on diffing by reference (rather than value) for strings. Replaces the StrExt::fast_diff method.
  • static disables diffing and prevents updates to the DOM after initial render of the value. Replaces the Stringify::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 the fence 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:

image

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

  1. The old methods added by extension traits are still supported with deprecation warnings and will be removed in the 0.7 release.

0.5

24 Mar 14:28
48e28a9
Compare
Choose a tag to compare
0.5

First publicly announced release! 🎉

Detailed description at https://maciej.codes/2023-03-23-kobold.html