Kobold logo

Kobold

Easy declarative web interfaces.

Kobold uses macros to deliver familiar HTML-esque syntax for building declarative web interfaces, while leveraging Rust's powerful type system for safety and performance.

Zero-Cost Static HTML

Like in React or Yew updates are done by repeating calls to a render function whenever the state changes. However, unlike either, Kobold does not produce a full blown virtual DOM. Instead the html! macro compiles all static HTML elements to a single JavaScript function that constructs the exact DOM for it.

All expressions, which must implement the Html trait, are injected into the constructed DOM on first render. Kobold keeps track of the DOM node references for these expressions. Since the exact types the expressions evaluate to are known to the Rust compiler, update calls can diff them by value and surgically update the DOM should they change. Changing a string or an integer only updates the exact Text node that string or integer was rendered to.

If the html! macro invocation contains HTML elements with no expressions, the constructed Html type will be zero-sized, and its Html::update method will be empty, making updates of static HTML quite literally zero-cost.

Hello World

Any struct that implements a render method can be used as a component:

```rust use kobold::prelude::*;

struct Hello { name: &'static str, }

impl Hello { fn render(self) -> impl Html { html! {

"Hello "{ self.name }"!"

} } }

fn main() { kobold::start(html! { }); } ```

The render method here will return a transient type that contains only the &'static str from the { self.name } expression. Kobold will create a text node for that string, and then send it to a compiled JavaScript function that will build the h1 element with the static text around it.

Everything is statically typed and the macro doesn't delete any information when manipulating the token stream, so the Rust compiler can tell you if you've made a mistake:

text error[E0560]: struct `Hello` has no field named `nam` --> examples/hello_world/src/main.rs:17:16 | 17 | <Hello nam="Kobold" /> | ^^^ help: a field with a similar name exists: `name`

You can even use rust-analyzer to refactor component or field names, and it will change the invocations inside the macros for you.

Stateful Components

The Stateful trait can be used to create components that own and manipulate their state:

```rust use kobold::prelude::*;

// To derive Stateful the component must also implement PartialEq.

[derive(Stateful, PartialEq, Default)]

struct Counter { count: u32, }

impl Counter { fn render(self) -> impl Html { self.stateful(|state, ctx| { let onclick = ctx.bind(|state, _event| state.count += 1);

        html! {
            <p>
                "You clicked on the "
                // `{onclick}` here is shorthand for `onclick={onclick}`
                <button {onclick}>"Button"</button>
                " "{ state.count }" times."
            </p>
        }
    })
}

}

fn main() { kobold::start(html! { // The .. notation fills in the rest of the component with // values from the Default impl. }); } ```

The stateful method above accepts a non-capturing anonymous render function matching the signature:

rust fn(&State, Context<State>) -> impl Html

The State here is an associated type which for all components that use derived Stateful implementation defaults to Self, so in the example above it is the Counter itself.

The Context can be used to create event callbacks that take a &mut reference to the state and a & reference to a DOM Event (ignored above). If the callback closure has no return type (the return type is ()) each invocation of it will update the component. If you would rather perform a "silent" update, or if the callback does not always modify the state, return the provided ShouldRender enum instead.

For more details visit the stateful module documentation.

Conditional Rendering

Because the html! macro produces unique transient types, if and match expressions that invoke the macro will naturally fail to compile. To fix this annotate a function with #[kobold::branching]:

```rs

[kobold::branching]

fn conditional(illuminatus: bool) -> impl Html { if illuminatus { html! {

"It was the year when they finally immanentized the Eschaton."

} } else { html! {
"It was love at first sight."
} } } ```

For more details visit the branching module documentation.

Lists and Iterators

To render an iterator use the list method from the ListIteratorExtextension trait:

rs fn make_list(count: u32) -> impl Html { html! { <ul> { (1..=count) .map(|n| html! { <li>"Item #"{n}</li> }) .list() } </ul> } }

This wraps the iterator in a transparent List<_> type that implements Html. On updates the iterator is consumed once and all items are diffed with previous version. No allocations are made by Kobold unless the rendered list needs to grow past its original capacity.

Borrowed Values

Html types are truly transient and only need to live for the duration of the initial render, or for the duration of the subsequent update. This means that you can easily and cheaply render borrowed state without unnecessary clones:

rs // Need to mark the return type with an elided lifetime // to tell the compiler that we borrow from `names` here fn render_names(names: &[String]) -> impl Html + '_ { html! { <ul> { names .iter() .map(|name| html! { <li>{ name }</li> }) .list() } </ul> } }

Components with children

If you wish to capture children from parent html! invocation, simply implement a render_with method on the component:

```rust use kobold::prelude::*;

struct Header;

impl Header { fn render_with(self, children: impl Html) -> impl Html { html! {

{ children }

} } }

fn main() { kobold::start(html! {

"Hello Kobold"
}); } ```

If you know or expect children to be of a specific type, you can do that too:

```rust use kobold::prelude::*;

struct AddTen;

impl AddTen { // integers implement Html so they can be passed by value fn render_with(self, n: i32) -> i32 { n + 10 } }

fn main() { kobold::start(html! {

"Meaning of life is " { 32 }

}); } ```

A component can have both render and render_with methods if you want to support both styles of invocation.

More examples

To run Kobold you'll need to install trunk: sh cargo install --locked trunk

You might also need to add the Wasm target to Rust: sh rustup target add wasm32-unknown-unknown

Then just run an example: ```sh

Go to an example

cd examples/counter

Run with trunk

trunk serve ```

Acknowledgements

License

Kobold is free software, and is released under the terms of the GNU Lesser General Public License version 3. See LICENSE.