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.
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.
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! {
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.
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
.
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.
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
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.
To render an iterator use the list
method from the ListIteratorExt
extension 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.
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>
}
}
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! {
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 "
A component can have both render
and render_with
methods if you want to
support both styles of invocation.
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
cd examples/counter
trunk serve ```
Kobold is free software, and is released under the terms of the GNU Lesser General Public License version 3. See LICENSE.