├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ci └── miri.sh ├── crates ├── kobold │ ├── Cargo.toml │ ├── js │ │ └── util.js │ └── src │ │ ├── attribute.rs │ │ ├── branching.rs │ │ ├── diff.rs │ │ ├── diff │ │ └── vstring.rs │ │ ├── dom.rs │ │ ├── event.rs │ │ ├── internal.rs │ │ ├── keywords.rs │ │ ├── lib.rs │ │ ├── list.rs │ │ ├── list │ │ ├── bounded.rs │ │ └── unbounded.rs │ │ ├── maybe.rs │ │ ├── stateful.rs │ │ ├── stateful │ │ ├── cell.rs │ │ ├── hook.rs │ │ ├── into_state.rs │ │ ├── product.rs │ │ └── should_render.rs │ │ └── value.rs ├── kobold_macros │ ├── Cargo.toml │ └── src │ │ ├── branching.rs │ │ ├── branching │ │ ├── ast.rs │ │ ├── parse.rs │ │ └── tokenize.rs │ │ ├── class.rs │ │ ├── dom.rs │ │ ├── dom │ │ ├── els.rs │ │ ├── expression.rs │ │ └── shallow.rs │ │ ├── fn_component.rs │ │ ├── fn_component │ │ └── generic_finder.rs │ │ ├── gen.rs │ │ ├── gen │ │ ├── component.rs │ │ ├── element.rs │ │ ├── fragment.rs │ │ └── transient.rs │ │ ├── itertools.rs │ │ ├── lib.rs │ │ ├── parse.rs │ │ ├── syntax.rs │ │ └── tokenize.rs ├── kobold_qr │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── kobold_router │ ├── Cargo.toml │ ├── js │ └── util.js │ └── src │ ├── internal.rs │ └── lib.rs ├── examples ├── counter │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── csv_editor │ ├── Cargo.toml │ ├── index.html │ └── src │ │ ├── csv.rs │ │ ├── main.rs │ │ └── state.rs ├── hello_world │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── interval │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── list │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── optional_params │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── qrcode │ ├── Cargo.toml │ ├── Trunk.toml │ ├── bundle.sh │ ├── index.html │ └── src │ │ └── main.rs ├── router │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── stateful │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs └── todomvc │ ├── Cargo.toml │ ├── Trunk.toml │ ├── bundle.sh │ ├── index.html │ ├── package.json │ └── src │ ├── main.rs │ └── state.rs └── kobold.svg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | checkfmt: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: cargo check --features serde 18 | - run: cargo fmt --check 19 | 20 | miri: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | - name: Run miri 28 | run: ci/miri.sh 29 | 30 | tests: 31 | strategy: 32 | matrix: 33 | rust: 34 | - stable 35 | - nightly 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: ${{ matrix.rust }} 42 | override: true 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: test 46 | args: --verbose 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | target 4 | **/*.rs.bk 5 | examples/*/dist 6 | examples/*/Cargo.lock 7 | crates/*/Cargo.lock 8 | .DS_Store 9 | .idea 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | "examples/*", 5 | ] 6 | resolver = "2" 7 | 8 | [profile.dev] 9 | opt-level = 3 10 | 11 | [profile.release] 12 | lto = "fat" 13 | codegen-units = 1 14 | strip = "symbols" 15 | panic = "abort" 16 | 17 | [profile.bench] 18 | lto = "fat" 19 | codegen-units = 1 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kobold 2 | 3 | Kobold logo 4 | 5 | [![Join Discord](https://img.shields.io/discord/1092732324610850816?label=&logo=discord&labelColor=6A7EC2&logoColor=ffffff&color=7389D8)](https://discord.gg/tgNX5VYSWF) 6 | [![Test](https://img.shields.io/github/actions/workflow/status/maciejhirsz/kobold/ci.yml?label=tests)](https://github.com/maciejhirsz/kobold/actions/workflows/ci.yml) 7 | [![Docs](https://img.shields.io/docsrs/kobold/latest)](https://docs.rs/kobold) 8 | [![Crates.io](https://img.shields.io/crates/v/kobold.svg)](https://crates.io/crates/kobold) 9 | [![MPL-2.0](https://img.shields.io/crates/l/kobold.svg?label=)](https://www.mozilla.org/en-US/MPL/) 10 | 11 | _Easy declarative web interfaces._ 12 | 13 | Key features: 14 | 15 | * Declarative [`view!`](https://docs.rs/kobold/latest/kobold/macro.view.html) macro that uses HTML-esque syntax complete with optional closing tags. 16 | * Functional [components](https://docs.rs/kobold/latest/kobold/attr.component.html) with optional parameters. 17 | * State management and event handling. 18 | * High performance and consistently the lowest Wasm footprint in the Rust ecosystem. 19 | 20 | ### Zero-Cost Static HTML 21 | 22 | The `view!` macro produces opaque `impl View` types that by default do no allocations. 23 | All static [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) elements compile to 24 | inline JavaScript code that constructs them. Expressions are injected into the constructed DOM on first render. 25 | Kobold keeps track of the DOM node references for these expressions. 26 | 27 | Since the exact types the expressions evaluate to are known to the Rust compiler, update calls can diff them by 28 | value (or pointer) and surgically update the DOM should they change. Changing a 29 | string or an integer only updates the exact [`Text` node](https://developer.mozilla.org/en-US/docs/Web/API/Text) 30 | that string or integer was rendered to. 31 | 32 | _If the `view!` macro invocation contains DOM elements with no expressions, the constructed `View` 33 | type will be zero-sized, and its `View::update` method will be empty, making updates of static 34 | DOM literally zero-cost._ 35 | 36 | ### Hello World! 37 | 38 | Components in **Kobold** are created by annotating a _render function_ with a `#[component]` attribute. 39 | 40 | ```rust 41 | use kobold::prelude::*; 42 | 43 | #[component] 44 | fn hello(name: &str) -> impl View + '_ { 45 | view! { 46 |

"Hello "{ name }"!"

47 | } 48 | } 49 | 50 | fn main() { 51 | kobold::start(view! { 52 | 53 | }); 54 | } 55 | ``` 56 | 57 | The component function must return a type that implements the `View` trait. Since the `view!` macro 58 | produces transient locally defined types the best approach here is to always use the opaque `impl View` return type. 59 | 60 | Everything here is statically typed and the macro doesn't delete any information when manipulating the 61 | token stream, so the Rust compiler can tell you when you've made a mistake: 62 | 63 | ```text 64 | error[E0560]: struct `Hello` has no field named `nam` 65 | --> examples/hello_world/src/main.rs:12:16 66 | | 67 | 12 | 68 | | ^^^ help: there is a method with a similar name: `name` 69 | ``` 70 | 71 | You can even use [rust-analyzer](https://rust-analyzer.github.io/) to refactor component or field names, 72 | and it will change the invocations inside the macros for you. 73 | 74 | ### State management 75 | 76 | The `stateful` function can be used to create views that own and manipulate 77 | their state: 78 | 79 | ```rust 80 | use kobold::prelude::*; 81 | 82 | #[component] 83 | fn counter(init: u32) -> impl View { 84 | stateful(init, |count| { 85 | bind! { count: 86 | // Create an event handler with access to `&mut u32` 87 | let onclick = move |_event| *count += 1; 88 | } 89 | 90 | view! { 91 |

92 | "You clicked the " 93 | // `{onclick}` here is shorthand for `onclick={onclick}` 94 | 95 | " "{ count }" times." 96 |

97 | } 98 | }) 99 | } 100 | 101 | fn main() { 102 | kobold::start(view! { 103 | 104 | }); 105 | } 106 | ``` 107 | 108 | The `stateful` function takes two parameters: 109 | 110 | * State constructor that implements the `IntoState` trait. **Kobold** comes with default 111 | implementations for most primitive types, so we can use `u32` here. 112 | * The anonymous render closure that uses the constructed state, in our case its argument is `&Hook`. 113 | 114 | The `Hook` here is a smart pointer to the state itself that allows non-mutable access to the 115 | state. The `bind!` macro can be invoked for any `Hook` to create closures with `&mut` references to the 116 | underlying state. 117 | 118 | For more details visit the [`stateful` module documentation](https://docs.rs/kobold/latest/kobold/stateful/index.html). 119 | 120 | ### Optional parameters 121 | 122 | Use `#[component(?)]` syntax to set a component parameter as default: 123 | 124 | ```rust 125 | // `code` will default to `200` if omitted 126 | #[component(code?: 200)] 127 | fn status(code: u32) -> impl View { 128 | view! { 129 |

"Status code was "{ code } 130 | } 131 | } 132 | 133 | view! { 134 | // Status code was 200 135 | 136 | // Status code was 404 137 | 138 | } 139 | ``` 140 | 141 | For more details visit the [`#[component]` macro documentation](https://docs.rs/kobold/latest/kobold/attr.component.html#optional-parameters-componentparam). 142 | 143 | ### Conditional Rendering 144 | 145 | Because the `view!` macro produces unique transient types, `if` and `match` expressions that invoke 146 | the macro will naturally fail to compile. 147 | 148 | Using the `auto_branch` flag on the `#[component]` attribute 149 | **Kobold** will scan the body of of your component render function, and make all `view!` macro invocations 150 | inside an `if` or `match` expression, and wrap them in an enum making them the same type: 151 | 152 | ```rust 153 | #[component(auto_branch)] 154 | fn conditional(illuminatus: bool) -> impl View { 155 | if illuminatus { 156 | view! {

"It was the year when they finally immanentized the Eschaton." } 157 | } else { 158 | view! {

"It was love at first sight." } 159 | } 160 | } 161 | ``` 162 | 163 | For more details visit the [`branching` module documentation](https://docs.rs/kobold/latest/kobold/branching/index.html). 164 | 165 | ### Lists and Iterators 166 | 167 | To render an iterator use the `for` keyword: 168 | 169 | ```rust 170 | use kobold::prelude::*; 171 | 172 | #[component] 173 | fn iterate_numbers(count: u32) -> impl View { 174 | view! { 175 |