diff --git a/Cargo.lock b/Cargo.lock index a0085ff4..3e4f3f8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ [[package]] name = "kobold" -version = "0.9.1" +version = "0.10.0" dependencies = [ "console_error_panic_hook", "itoa", @@ -208,7 +208,7 @@ dependencies = [ [[package]] name = "kobold_macros" -version = "0.9.0" +version = "0.10.0" dependencies = [ "arrayvec", "beef", @@ -300,15 +300,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 3617cda8..483ee309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/*", "examples/*", ] +resolver = "2" [profile.release] lto = "fat" diff --git a/README.md b/README.md index 1332b40e..5055d4eb 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Components in **Kobold** are created by annotating a _render function_ with a `# use kobold::prelude::*; #[component] -fn Hello(name: &str) -> impl View + '_ { +fn hello(name: &str) -> impl View + '_ { view! {

"Hello "{ name }"!"

} @@ -49,7 +49,7 @@ fn Hello(name: &str) -> impl View + '_ { fn main() { kobold::start(view! { - + }); } ``` @@ -64,8 +64,8 @@ token stream, so the Rust compiler can tell you when you've made a mistake: error[E0560]: struct `Hello` has no field named `nam` --> examples/hello_world/src/main.rs:12:16 | -12 | - | ^^^ help: a field with a similar name exists: `name` +12 | + | ^^^ help: there is a method with a similar name: `name` ``` You can even use [rust-analyzer](https://rust-analyzer.github.io/) to refactor component or field names, @@ -80,7 +80,7 @@ their state: use kobold::prelude::*; #[component] -fn Counter(init: u32) -> impl View { +fn counter(init: u32) -> impl View { stateful(init, |count| { bind! { count: // Create an event handler with access to `&mut u32` @@ -100,7 +100,7 @@ fn Counter(init: u32) -> impl View { fn main() { kobold::start(view! { - + }); } ``` @@ -109,7 +109,7 @@ The `stateful` function takes two parameters: * State constructor that implements the `IntoState` trait. **Kobold** comes with default implementations for most primitive types, so we can use `u32` here. -* The anonymous render function that uses the constructed state, in our case its argument is `&Hook`. +* The anonymous render closure that uses the constructed state, in our case its argument is `&Hook`. The `Hook` here is a smart pointer to the state itself that allows non-mutable access to the state. The `bind!` macro can be invoked for any `Hook` to create closures with `&mut` references to the @@ -124,7 +124,7 @@ Use `#[component(?)]` syntax to set a component parameter as default: ```rust // `code` will default to `200` if omitted #[component(code?: 200)] -fn Status(code: u32) -> impl View { +fn status(code: u32) -> impl View { view! {

"Status code was "{ code } } @@ -132,9 +132,9 @@ fn Status(code: u32) -> impl View { view! { // Status code was 200 - + // Status code was 404 - + } ``` @@ -151,7 +151,7 @@ inside an `if` or `match` expression, and wrap them in an enum making them the s ```rust #[component(auto_branch)] -fn Conditional(illuminatus: bool) -> impl View { +fn conditional(illuminatus: bool) -> impl View { if illuminatus { view! {

"It was the year when they finally immanentized the Eschaton." } } else { @@ -167,11 +167,10 @@ For more details visit the [`branching` module documentation](https://docs.rs/ko To render an iterator use the `for` keyword: ```rust -// `ListIteratorExt` is included in the prelude use kobold::prelude::*; #[component] -fn IterateNumbers(count: u32) -> impl View { +fn iterate_numbers(count: u32) -> impl View { view! {

    { @@ -195,7 +194,7 @@ state without unnecessary clones: ```rust #[component] -fn Users<'a>(names: &'a [&'a str]) -> impl View + 'a { +fn users<'a>(names: &'a [&'a str]) -> impl View + 'a { view! {
      { @@ -214,7 +213,7 @@ If you wish to capture children from parent `view!` invocation, simply change use kobold::prelude::*; #[component(children)] -fn Header(children: impl View) -> impl View { +fn header(children: impl View) -> impl View { view! {

      { children }

      } @@ -222,7 +221,7 @@ fn Header(children: impl View) -> impl View { fn main() { kobold::start(view! { -
      "Hello Kobold"
      + "Hello Kobold" }); } ``` @@ -234,7 +233,7 @@ use kobold::prelude::*; // Capture children into the argument `n` #[component(children: n)] -fn AddTen(n: i32) -> i32 { +fn add_ten(n: i32) -> i32 { // integers implement `View` so they can be passed by value n + 10 } @@ -243,7 +242,7 @@ fn main() { kobold::start(view! {

      "Meaning of life is " - { 32 } + { 32 }

      }); } @@ -251,7 +250,7 @@ fn main() { ## More Examples -To run **Kobold** you'll need to install [`trunk`](https://trunkrs.dev/) (check the [full instructions](https://trunkrs.dev/#install) if you have problems): +To run **Kobold** you'll need to install [`trunk`](https://trunkrs.dev/): ```sh cargo install --locked trunk ``` diff --git a/crates/kobold/Cargo.toml b/crates/kobold/Cargo.toml index 9a30e901..2dee2952 100644 --- a/crates/kobold/Cargo.toml +++ b/crates/kobold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kobold" -version = "0.9.1" +version = "0.10.0" authors = ["Maciej Hirsz "] edition = "2021" license = "MPL-2.0" @@ -19,7 +19,7 @@ stateful = [] wasm-bindgen = "0.2.84" wasm-bindgen-futures = "0.4.34" itoa = "1.0.6" -kobold_macros = { version = "0.9.0", path = "../kobold_macros" } +kobold_macros = { version = "0.10.0", path = "../kobold_macros" } console_error_panic_hook = "0.1.7" rlsf = { version = "0.2.1", optional = true } serde = { version = "1", optional = true } diff --git a/crates/kobold/js/util.js b/crates/kobold/js/util.js index cd701a72..f705f994 100644 --- a/crates/kobold/js/util.js +++ b/crates/kobold/js/util.js @@ -44,11 +44,9 @@ export function removeClass(n,v) { n.classList.remove(v); } export function replaceClass(n,o,v) { n.classList.replace(o,v); } export function toggleClass(n,c,v) { n.classList.toggle(c,v); } -export function makeEventHandler(c,f) { return (e) => koboldCallback(e,c,f); } -export function checkEventHandler() { if (typeof koboldCallback !== "function") console.error( -`Missing \`koboldCallback\` in global scope. -Add the following to your Trunk.toml: - -[build] -pattern_script = "" +export function makeEventHandler(c,f) { return (e) => wasmBindings.koboldCallback(e,c,f); } +export function checkEventHandler() { if (typeof wasmBindings !== "object") console.error( +`Missing \`wasmBindings\` in global scope. +As of Kobold v0.10 and Trunk v0.17.16 you no longer need to export bindings manually, \ +please remove the custom \`pattern_script\' from your \`Trunk.toml\` file. `) } diff --git a/crates/kobold/src/branching.rs b/crates/kobold/src/branching.rs index d5d96f7e..ce2c6235 100644 --- a/crates/kobold/src/branching.rs +++ b/crates/kobold/src/branching.rs @@ -9,7 +9,7 @@ //! ```compile_fail //! # use kobold::prelude::*; //! #[component] -//! fn Conditional(illuminatus: bool) -> impl View { +//! fn conditional(illuminatus: bool) -> impl View { //! if illuminatus { //! view! {

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

      } //! } else { @@ -38,13 +38,14 @@ //! ``` //! # use kobold::prelude::*; //! #[component(auto_branch)] -//! fn Conditional(illuminatus: bool) -> impl View { +//! fn conditional(illuminatus: bool) -> impl View { //! if illuminatus { //! view! {

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

      } //! } else { //! view! {
      "It was love at first sight."
      } //! } //! } +//! # fn main() {} //! ``` //! //! This flag is not enabled by default, yet, as there might be situations [`auto_branch`](crate::component#componentauto_branch) @@ -59,7 +60,7 @@ //! use kobold::branching::Branch2; //! //! #[component] -//! fn Conditional(illuminatus: bool) -> impl View { +//! fn conditional(illuminatus: bool) -> impl View { //! if illuminatus { //! Branch2::A(view! { //!

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

      @@ -70,16 +71,17 @@ //! }) //! } //! } +//! # fn main() {} //! ``` //! //! This is in fact all that the [`auto_branch`](crate::component#componentauto_branch) flag does for you automatically. //! -//! For simple optional renders you can always use the standard library [`Option`](Option): +//! For simple optional renders you can always use the standard library [`Option`]: //! //! ``` //! # use kobold::prelude::*; //! #[component] -//! fn Conditional(illuminatus: bool) -> impl View { +//! fn conditional(illuminatus: bool) -> impl View { //! if illuminatus { //! Some(view! { //!

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

      @@ -88,6 +90,7 @@ //! None //! } //! } +//! # fn main() {} //! ``` use std::mem::MaybeUninit; diff --git a/crates/kobold/src/diff.rs b/crates/kobold/src/diff.rs index 08621d26..9ee09c62 100644 --- a/crates/kobold/src/diff.rs +++ b/crates/kobold/src/diff.rs @@ -35,7 +35,7 @@ pub use vstring::VString; /// } /// /// #[component] -/// fn UserRow(user: &User) -> impl View + '_ { +/// fn user_row(user: &User) -> impl View + '_ { /// fence(user.id, || view! { /// // This row is only re-rendered if `user.id` has changed /// @@ -48,6 +48,7 @@ pub use vstring::VString; /// /// }) /// } +/// # fn main() {} /// ``` pub const fn fence(guard: D, render: F) -> Fence where @@ -61,7 +62,7 @@ where } } -/// Smart [`View`](View) that guards against unnecessary renders, see [`fence`](fence). +/// Smart [`View`] that guards against unnecessary renders, see [`fence`]. pub struct Fence { guard: D, inner: F, @@ -165,7 +166,7 @@ macro_rules! impl_diff { impl_diff_str!(&str, &String); impl_diff!(bool, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64); -/// Smart [`View`](View) that only updates its content when the reference to T has changed. +/// Smart [`View`] that only updates its content when the reference to T has changed. /// See [`ref`](crate::keywords::ref). #[repr(transparent)] pub struct Ref(T); @@ -203,7 +204,7 @@ impl Diff for &Ref { } } -/// Smart [`View`](View) that never performs diffing and instead always triggers +/// Smart [`View`] that never performs diffing and instead always triggers /// updates. /// /// See [`use`](crate::keywords::use) @@ -211,7 +212,7 @@ impl Diff for &Ref { #[repr(transparent)] pub struct Eager(pub(crate) T); -/// Smart [`View`](View) that never performs diffing and instead never triggers +/// Smart [`View`] that never performs diffing and instead never triggers /// updates. /// /// See [`static`](crate::keywords::static) diff --git a/crates/kobold/src/dom.rs b/crates/kobold/src/dom.rs index 67eba1a4..f6f9ba08 100644 --- a/crates/kobold/src/dom.rs +++ b/crates/kobold/src/dom.rs @@ -27,7 +27,7 @@ pub trait Mountable: 'static { fn replace_with(&self, new: &JsValue); } -/// A light-weight [`Deref`](Deref)-like trait that +/// A light-weight [`Deref`]-like trait that /// auto-implements `Mountable` by proxying it to another type. pub trait Anchor { type Js: JsCast; diff --git a/crates/kobold/src/internal.rs b/crates/kobold/src/internal.rs index 87b1d5b2..5aed365c 100644 --- a/crates/kobold/src/internal.rs +++ b/crates/kobold/src/internal.rs @@ -15,14 +15,14 @@ use crate::View; /// Uninitialized stable pointer to `T`. /// -/// Used for the initialize-in-place strategy employed by the [`View::build`](View::build) method. +/// Used for the initialize-in-place strategy employed by the [`View::build`] method. #[must_use] #[repr(transparent)] pub struct In<'a, T>(&'a mut MaybeUninit); /// Initialized stable pointer to `T`. /// -/// Used for the initialize-in-place strategy employed by the [`View::build`](View::build) method. +/// Used for the initialize-in-place strategy employed by the [`View::build`] method. #[repr(transparent)] pub struct Out<'a, T>(&'a mut T); @@ -174,7 +174,7 @@ macro_rules! init { }; } -/// Wrapper that turns `extern` precompiled JavaScript functions into [`View`](View)s. +/// Wrapper that turns `extern` precompiled JavaScript functions into [`View`]s. #[repr(transparent)] pub struct Precompiled(pub F); diff --git a/crates/kobold/src/keywords.rs b/crates/kobold/src/keywords.rs index 805f6b93..7217e6c7 100644 --- a/crates/kobold/src/keywords.rs +++ b/crates/kobold/src/keywords.rs @@ -8,7 +8,7 @@ use crate::diff::{Eager, Ref, Static}; use crate::list::List; use crate::View; -/// `{ for ... }`: turn an [`IntoIterator`](IntoIterator) type into a [`View`](View). +/// `{ for ... }`: turn an [`IntoIterator`] type into a [`View`]. /// /// ``` /// # use kobold::prelude::*; @@ -42,7 +42,7 @@ where /// } /// /// #[component] -/// fn UserRow(user: &User) -> impl View + '_ { +/// fn user_row(user: &User) -> impl View + '_ { /// view! { /// /// // If `name` and `email` are always sent to the UI as @@ -53,6 +53,7 @@ where /// /// } /// } +/// # fn main() {} /// ``` pub const fn r#ref(value: &str) -> &Ref { unsafe { &*(value as *const _ as *const Ref) } diff --git a/crates/kobold/src/lib.rs b/crates/kobold/src/lib.rs index b95f6952..f2b6f51f 100644 --- a/crates/kobold/src/lib.rs +++ b/crates/kobold/src/lib.rs @@ -27,8 +27,8 @@ //! string or an integer only updates the exact [`Text` node](https://developer.mozilla.org/en-US/docs/Web/API/Text) //! that string or integer was rendered to. //! -//! _If the [`view!`](view) macro invocation contains DOM elements with no expressions, the constructed [`View`](View) -//! type will be zero-sized, and its [`View::update`](View::update) method will be empty, making updates of static +//! _If the [`view!`](view) macro invocation contains DOM elements with no expressions, the constructed [`View`] +//! type will be zero-sized, and its [`View::update`] method will be empty, making updates of static //! HTML literally zero-cost._ //! //! ### Hello World! @@ -39,7 +39,7 @@ //! use kobold::prelude::*; //! //! #[component] -//! fn Hello(name: &str) -> impl View + '_ { +//! fn hello(name: &str) -> impl View + '_ { //! view! { //!

      "Hello "{ name }"!"

      //! } @@ -47,12 +47,12 @@ //! //! fn main() { //! kobold::start(view! { -//! +//! //! }); //! } //! ``` //! -//! The component function must return a type that implements the [`View`](View) trait. Since the [`view!`](view) macro +//! The component function must return a type that implements the [`View`] trait. Since the [`view!`](view) macro //! produces transient locally defined types the best approach here is to always use the opaque `impl View` return type. //! //! Everything here is statically typed and the macro doesn't delete any information when manipulating the @@ -62,8 +62,8 @@ //! error[E0560]: struct `Hello` has no field named `nam` //! --> examples/hello_world/src/main.rs:12:16 //! | -//! 12 | -//! | ^^^ help: a field with a similar name exists: `name` +//! 12 | +//! | ^^^ help: there is a method with a similar name: `name` //! ``` //! //! You can even use [rust-analyzer](https://rust-analyzer.github.io/) to refactor component or field names, @@ -78,7 +78,7 @@ //! use kobold::prelude::*; //! //! #[component] -//! fn Counter(init: u32) -> impl View { +//! fn counter(init: u32) -> impl View { //! stateful(init, |count| { //! bind! { count: //! // Create an event handler with access to `&mut u32` @@ -98,7 +98,7 @@ //! //! fn main() { //! kobold::start(view! { -//! +//! //! }); //! } //! ``` @@ -123,20 +123,21 @@ //! # use kobold::prelude::*; //! // `code` will default to `200` if omitted //! #[component(code?: 200)] -//! fn Status(code: u32) -> impl View { +//! fn status(code: u32) -> impl View { //! view! { //!

      "Status code was "{ code } //! } //! } //! +//! # fn main() { //! # let _ = //! view! { //! // Status code was 200 -//! +//! //! // Status code was 404 -//! +//! //! } -//! # ; +//! # ; } //! ``` //! //! For more details visit the [`#[component]` macro documentation](component#optional-parameters-componentparam). @@ -153,13 +154,14 @@ //! ``` //! # use kobold::prelude::*; //! #[component(auto_branch)] -//! fn Conditional(illuminatus: bool) -> impl View { +//! fn conditional(illuminatus: bool) -> impl View { //! if illuminatus { //! view! {

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

      "It was love at first sight." } //! } //! } +//! # fn main() {} //! ``` //! //! For more details visit the [`branching` module documentation](branching). @@ -172,7 +174,7 @@ //! use kobold::prelude::*; //! //! #[component] -//! fn IterateNumbers(count: u32) -> impl View { +//! fn iterate_numbers(count: u32) -> impl View { //! view! { //!
        //! { @@ -180,6 +182,7 @@ //! } //! } //! } +//! # fn main() {} //! ``` //! //! On updates the iterator is consumed once and all items are diffed with the previous version. @@ -190,14 +193,14 @@ //! //! ### Borrowed Values //! -//! [`View`](View) types are truly transient and only need to live for the duration of the initial render, +//! [`View`] 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: //! //! ``` //! # use kobold::prelude::*; //! #[component] -//! fn Users<'a>(names: &'a [&'a str]) -> impl View + 'a { +//! fn users<'a>(names: &'a [&'a str]) -> impl View + 'a { //! view! { //!
          //! { @@ -205,6 +208,7 @@ //! } //! } //! } +//! # fn main() {} //! ``` //! //! ### Components with Children @@ -216,7 +220,7 @@ //! use kobold::prelude::*; //! //! #[component(children)] -//! fn Header(children: impl View) -> impl View { +//! fn header(children: impl View) -> impl View { //! view! { //!

          { children }

          //! } @@ -224,7 +228,7 @@ //! //! fn main() { //! kobold::start(view! { -//!
          "Hello Kobold"
          +//! "Hello Kobold" //! }); //! } //! ``` @@ -236,7 +240,7 @@ //! //! // Capture children into the argument `n` //! #[component(children: n)] -//! fn AddTen(n: i32) -> i32 { +//! fn add_ten(n: i32) -> i32 { //! // integers implement `View` so they can be passed by value //! n + 10 //! } @@ -245,7 +249,7 @@ //! kobold::start(view! { //!

          //! "Meaning of life is " -//! { 32 } +//! { 32 } //!

          //! }); //! } @@ -280,11 +284,12 @@ /// ``` /// # use kobold::prelude::*; /// #[component] -/// fn MyComponent() -> impl View { +/// fn my_component() -> impl View { /// view! { ///

          "Hello, world!"

          /// } /// } +/// # fn main() {} /// ``` /// /// ## Flags @@ -296,7 +301,7 @@ /// /// Allows for parameters to have default values. Available syntax: /// -/// * `#[component(foo?)]`: mark the parameter `foo` as optional, use [`Default`](Default) trait implementation if absent. +/// * `#[component(foo?)]`: mark the parameter `foo` as optional, use [`Default`] trait implementation if absent. /// * `#[component(foo?: )]`: mark the parameter `foo` as optional, default to ``. /// /// #### Examples @@ -308,7 +313,7 @@ /// // Make `age` an optional parameter, use the `Default` value /// age?, /// )] -/// fn Greeter<'a>(name: &'a str, age: Option) -> impl View + 'a { +/// fn greeter<'a>(name: &'a str, age: Option) -> impl View + 'a { /// let age = age.map(|age| view!(", you are "{ age }" years old")); /// /// view! { @@ -316,44 +321,44 @@ /// } /// } /// -/// # let _ = +/// # fn main() { let _ = /// view! { /// // Hello Kobold -/// +/// /// // Hello Alice -/// +/// /// // Hello Bob, you are 42 years old -/// +/// /// } -/// # ; +/// # ; } /// ``` /// /// Optional parameters of any type `T` can be set using any type that implements /// [`Maybe`](crate::maybe::Maybe). /// -/// This allows you to set optional parameters using an [`Option`](Option): +/// This allows you to set optional parameters using an [`Option`]: /// ``` /// # use kobold::prelude::*; /// #[component(code?: 200)] -/// fn StatusCode(code: u32) -> impl View { +/// fn status_code(code: u32) -> impl View { /// view! { ///

          "Status code was "{ code } /// } /// } /// -/// # let _ = +/// # fn main() { let _ = /// view! { /// // Status code was 200 -/// +/// /// // Status code was 404 -/// +/// /// /// // Status code was 200 -/// +/// /// // Status code was 500 -/// +/// /// } -/// # ; +/// # ; } /// ``` /// /// All values are lazy-evaluated: @@ -362,16 +367,17 @@ /// # use kobold::prelude::*; /// // The owned `String` will only be created if the `name` is not set. /// #[component(name?: "Kobold".to_string())] -/// fn Greeter(name: String) -> impl View { +/// fn greeter(name: String) -> impl View { /// view! { ///

          "Hello "{ name } /// } /// } +/// # fn main() {} /// ``` /// /// #### 💡 Note: /// -/// You can only mark types that implement the [`Default`](Default) trait as optional, even if you provide +/// You can only mark types that implement the [`Default`] trait as optional, even if you provide /// a concrete value using `param?: value`. This requirement might be relaxed in the future when trait /// specialization is stabilized. /// @@ -390,7 +396,7 @@ /// * `#[component(children: my_name)]`: children will be captured by the `my_name` argument on the function. pub use kobold_macros::component; -/// Macro for creating transient [`View`](View) types. See the [main documentation](crate) for details. +/// Macro for creating transient [`View`] types. See the [main documentation](crate) for details. pub use kobold_macros::view; use wasm_bindgen::JsCast; @@ -533,7 +539,7 @@ where } } -/// Start the Kobold app by mounting given [`View`](View) in the document `body`. +/// Start the Kobold app by mounting given [`View`] in the document `body`. pub fn start(view: impl View) { init_panic_hook(); @@ -556,7 +562,7 @@ fn init_panic_hook() { use std::cell::Cell; thread_local! { - static INIT: Cell = Cell::new(false); + static INIT: Cell = const { Cell::new(false) }; } if !INIT.with(|init| init.get()) { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); diff --git a/crates/kobold/src/maybe.rs b/crates/kobold/src/maybe.rs index cf0770db..7cf5d690 100644 --- a/crates/kobold/src/maybe.rs +++ b/crates/kobold/src/maybe.rs @@ -2,12 +2,12 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! The [`Maybe`](Maybe) trait and its implementations +//! The [`Maybe`] trait and its implementations /// Undefined component parameter. If you've encountered this type it usually means /// you've failed to set a required component parameter. /// -/// This is a zero-sized type that implements [`Maybe`](Maybe) for all `T: Default`. +/// This is a zero-sized type that implements [`Maybe`](Maybe) for all `T`. pub struct Undefined; /// Helper trait for handling optional parameters in components. diff --git a/crates/kobold/src/stateful.rs b/crates/kobold/src/stateful.rs index 80232f4b..457a41e0 100644 --- a/crates/kobold/src/stateful.rs +++ b/crates/kobold/src/stateful.rs @@ -8,7 +8,7 @@ //! is no way to update them short of the parent view re-rendering them. //! //! However a fully functional app like that wouldn't be very useful, as all it -//! could ever do is render itself once. To get around this the [`stateful`](stateful) function can +//! could ever do is render itself once. To get around this the [`stateful`] function can //! be used to create views that have ownership over some arbitrary mutable state. //! use std::cell::UnsafeCell; @@ -50,8 +50,8 @@ pub struct StatefulProduct { inner: Rc>, } -/// Create a stateful [`View`](crate::View) over some mutable state. The state -/// needs to be created using the [`IntoState`](IntoState) trait. +/// Create a stateful [`View`] over some mutable state. The state +/// needs to be created using the [`IntoState`] trait. /// /// ``` /// # use::kobold::prelude::*; diff --git a/crates/kobold/src/stateful/hook.rs b/crates/kobold/src/stateful/hook.rs index 66cc80f9..18ddd4ef 100644 --- a/crates/kobold/src/stateful/hook.rs +++ b/crates/kobold/src/stateful/hook.rs @@ -283,6 +283,6 @@ mod test { }; // Make sure we can copy the mock twice - drop([mock, mock]); + let _ = [mock, mock]; } } diff --git a/crates/kobold/src/value.rs b/crates/kobold/src/value.rs index 87f7b105..0905f81b 100644 --- a/crates/kobold/src/value.rs +++ b/crates/kobold/src/value.rs @@ -81,7 +81,7 @@ impl View for String { p.put(TextProduct { memo: self, node }) } - fn update(self, mut p: &mut Self::Product) { + fn update(self, p: &mut Self::Product) { if p.memo != self { p.memo = self; p.memo.set_prop(TextContent, &p.node); diff --git a/crates/kobold_macros/Cargo.toml b/crates/kobold_macros/Cargo.toml index 72a007fc..7629f465 100644 --- a/crates/kobold_macros/Cargo.toml +++ b/crates/kobold_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kobold_macros" -version = "0.9.0" +version = "0.10.0" authors = ["Maciej Hirsz "] edition = "2021" license = "MPL-2.0" @@ -15,10 +15,10 @@ documentation = "https://docs.rs/kobold" arrayvec = "0.7.2" beef = "0.5.2" fnv = "1.0.7" -once_cell = "1.17.1" +once_cell = "1.19.0" [dev-dependencies] -proc-macro2 = "1.0.51" +proc-macro2 = "1.0.79" [lib] proc-macro = true diff --git a/crates/kobold_macros/src/branching/parse.rs b/crates/kobold_macros/src/branching/parse.rs index c66c6e49..c9ecd200 100644 --- a/crates/kobold_macros/src/branching/parse.rs +++ b/crates/kobold_macros/src/branching/parse.rs @@ -158,7 +158,7 @@ fn parse_code(stream: &mut ParseStream, scope: Branches) -> Result, Pa match maybe_html.into_inner() { Ok(html) => { - let branches = scope.map(Clone::clone); + let branches = scope.cloned(); let span = html[0].span(); code.push(Code::Scoped(Scoped::new( diff --git a/crates/kobold_macros/src/dom.rs b/crates/kobold_macros/src/dom.rs index 98519161..49556f9b 100644 --- a/crates/kobold_macros/src/dom.rs +++ b/crates/kobold_macros/src/dom.rs @@ -21,9 +21,7 @@ pub fn parse(tokens: TokenStream) -> Result, ParseError> { let mut nodes = Vec::new(); - while let Some(node) = Node::parse(&mut stream)? { - nodes.push(node); - } + while Node::parse(&mut stream, &mut nodes)? > 0 {} if nodes.is_empty() { return Err(ParseError::new("Empty view! invocation", Span::call_site())); @@ -91,23 +89,23 @@ impl From for AttributeValue { } impl Node { - fn parse(stream: &mut ShallowStream) -> Result, ParseError> { + fn parse(stream: &mut ShallowStream, parent: &mut Vec) -> Result { let tag = match stream.next() { Some(Ok(ShallowNode::Tag(tag))) => tag, Some(Ok(ShallowNode::Literal(lit))) => { - return Ok(Some(Node::Text(lit))); + parent.push(Node::Text(lit)); + return Ok(1); } Some(Ok(ShallowNode::Expression(expr))) => { - return Ok(Some(Expression::from(expr).into())); - } - Some(Err(error)) => { - return Err(error.msg("Expected a tag, a string literal, or an {expression}")) + parent.push(Expression::from(expr).into()); + return Ok(1); } - None => return Ok(None), + Some(Err(error)) => return Err(error), + None => return Ok(0), }; let children = match tag.nesting { - TagNesting::SelfClosing => None, + TagNesting::SelfClosing => Children::None, TagNesting::Opening => Node::parse_children(&tag.name, stream)?, TagNesting::Closing => { return Err(ParseError::new( @@ -131,14 +129,26 @@ impl Node { props.push(content.parse()?); } - Ok(Some(Node::Component(Component { + let (children, tail) = match children { + Children::None => (None, Vec::new()), + Children::Explicit(children) => (Some(children), Vec::new()), + Children::Implicit(tail) => (None, tail), + }; + + let count = 1 + tail.len(); + + parent.push(Node::Component(Component { name, span, path, generics, props, children, - }))) + })); + + parent.extend(tail); + + Ok(count) } TagName::HtmlElement { name, span } => { let mut content = tag.content.parse_stream(); @@ -174,28 +184,34 @@ impl Node { } } - Ok(Some(Node::HtmlElement(HtmlElement { + let children = match children { + Children::None => None, + Children::Explicit(children) | Children::Implicit(children) => Some(children), + }; + + parent.push(Node::HtmlElement(HtmlElement { name, span, classes, attributes, children, - }))) + })); + + Ok(1) } } } - fn parse_children( - name: &TagName, - stream: &mut ShallowStream, - ) -> Result>, ParseError> { + fn parse_children(name: &TagName, stream: &mut ShallowStream) -> Result { let mut children = Vec::new(); + let mut explicit = false; loop { if let Some(Ok(ShallowNode::Tag(tag))) = stream.peek() { match tag.is_closing(name) { IsClosing::Explicit => { stream.next(); + explicit = true; break; } IsClosing::Implicit => { @@ -205,20 +221,27 @@ impl Node { } } - match Node::parse(stream)? { - Some(node) => children.push(node), - None => break, + if Node::parse(stream, &mut children)? == 0 { + break; } } if children.is_empty() { - Ok(None) + Ok(Children::None) + } else if explicit { + Ok(Children::Explicit(children)) } else { - Ok(Some(children)) + Ok(Children::Implicit(children)) } } } +enum Children { + None, + Explicit(Vec), + Implicit(Vec), +} + impl Parse for Property { fn parse(stream: &mut ParseStream) -> Result { // Allow expression shorthand diff --git a/crates/kobold_macros/src/dom/shallow.rs b/crates/kobold_macros/src/dom/shallow.rs index e6958b90..54aa0057 100644 --- a/crates/kobold_macros/src/dom/shallow.rs +++ b/crates/kobold_macros/src/dom/shallow.rs @@ -121,17 +121,22 @@ impl Display for TagName { impl Parse for TagName { fn parse(stream: &mut ParseStream) -> Result { + let escaped = stream.allow_consume('!').is_some(); + let mut ident: Ident = stream.parse()?; let mut span = ident.span(); - if !stream.allow((':', Spacing::Joint)) { - if let Some(name) = ident.with_str(ElementTag::from_str) { - return Ok(TagName::HtmlElement { name, span }); - } + if !escaped { + return match ident.with_str(ElementTag::from_str) { + Some(name) => return Ok(TagName::HtmlElement { name, span }), + None => Err(ParseError::new( + format!("Unknown tag name `{ident}`. Did you mean a component ``?"), + span, + )), + }; } let mut name = ident.to_string(); - let mut path = ident.tokenize(); while let Some(colon) = stream.allow_consume((':', Spacing::Joint)) { diff --git a/crates/kobold_macros/src/fn_component.rs b/crates/kobold_macros/src/fn_component.rs index 3ede9508..7002f1d5 100644 --- a/crates/kobold_macros/src/fn_component.rs +++ b/crates/kobold_macros/src/fn_component.rs @@ -4,7 +4,7 @@ use std::fmt::Write; -use tokens::{Ident, TokenStream, TokenTree}; +use tokens::{Group, Ident, TokenStream, TokenTree}; use crate::parse::prelude::*; use crate::tokenize::prelude::*; @@ -124,44 +124,46 @@ struct Function { r#pub: Option, name: Ident, generics: Option, + raw_args: Option, arguments: Vec, r#return: TokenStream, body: TokenTree, } struct FnComponent { - r#struct: Ident, + r#fn: TokenTree, + r#mod: Ident, r#pub: Option, name: Ident, generics: Option, + raw_args: Option, arguments: Vec, ret: TokenStream, render: TokenStream, - children: Option, } impl FnComponent { fn new(args: &mut ComponentArgs, mut fun: Function) -> Result { - let children = match &args.children { - Some(children) => { - let ident = children.to_string(); - - let children_idx = fun.arguments.iter().position(|arg| arg.name.eq_str(&ident)); - - match children_idx { - Some(idx) => Some(fun.arguments.remove(idx)), - None => { - return Err(ParseError::new( - format!( - "Missing argument `{ident}` required to capture component children" - ), - children.span(), - )); - } + if let Some(children) = args.children.take() { + let ident = children.to_string(); + let mut found = false; + + for arg in fun.arguments.iter_mut() { + if arg.name.eq_str(&ident) { + arg.name = Ident::new("children", arg.name.span()); + + found = true; + break; } } - None => None, - }; + + if !found { + return Err(ParseError::new( + format!("Missing argument `{ident}` required to capture component children"), + children.span(), + )); + } + } let mut temp_var = String::with_capacity(40); @@ -188,17 +190,18 @@ impl FnComponent { tt => tt.into(), }; - let r#struct = Ident::new("struct", fun.r#fn.span()); + let r#mod = Ident::new("mod", fun.r#fn.span()); Ok(FnComponent { - r#struct, + r#fn: fun.r#fn, + r#mod, r#pub: fun.r#pub, name: fun.name, generics: fun.generics, + raw_args: fun.raw_args, arguments: fun.arguments, ret: fun.r#return, render, - children, }) } } @@ -227,8 +230,10 @@ impl Parse for Function { }; let mut arguments = Vec::new(); + let mut raw_args = None; if let TokenTree::Group(args) = stream.expect('(')? { + raw_args = Some(args.clone()); let mut stream = args.stream().parse_stream(); while !stream.end() { @@ -256,6 +261,7 @@ impl Parse for Function { r#pub, name, generics, + raw_args, arguments, r#return: ret, body, @@ -283,6 +289,7 @@ impl Parse for Argument { impl Tokenize for FnComponent { fn tokenize_in(self, out: &mut TokenStream) { + let mut mo = TokenStream::new(); let name = &self.name; let mut finder: Option = match &self.generics { @@ -290,39 +297,24 @@ impl Tokenize for FnComponent { Some(generics) => generics.tokens.clone().parse_stream().parse().ok(), }; - let mut args = if self.arguments.is_empty() { - ("_:", name).tokenize() + let args = if self.arguments.is_empty() { + "_: Props".tokenize() } else { - let destruct = (name, block(each(self.arguments.iter().map(Argument::name)))); - let props_ty = ( - name, - '<', - each(self.arguments.iter().map(Argument::ty)), - '>', + let destruct = ( + "Props", + block(each(self.arguments.iter().map(Argument::name))), ); + let props_ty = ("Props<", each(self.arguments.iter().map(Argument::ty)), '>'); (destruct, ':', props_ty).tokenize() }; - let fn_render = match self.children { - Some(children) => { - args.write((',', children)); - "#[doc(hidden)] pub fn __render_with" - } - None => "#[doc(hidden)] pub fn __render", - }; - - out.write(( - "#[allow(non_camel_case_types)]", - self.r#pub, - self.r#struct, - name, - )); + mo.write("#[allow(non_camel_case_types)] pub struct Props"); if self.arguments.is_empty() { - out.write(';'); + mo.write(';'); } else { - out.write(( + mo.write(( '<', each(self.arguments.iter().map(Argument::generic)).tokenize(), '>', @@ -331,36 +323,58 @@ impl Tokenize for FnComponent { }; let fn_render = ( - fn_render, - self.generics, + "pub fn render", + self.generics.clone(), group('(', args), - self.ret, + self.ret.clone(), block(( each(self.arguments.iter().map(Argument::maybe)), - self.render, + call( + ("super::", name), + each(self.arguments.iter().map(Argument::name)), + ), )), ); let fn_props = ( - "#[doc(hidden)] pub const fn __undefined() -> Self", + "pub const fn props() -> Props", block(( - "Self", + "Props", block(each(self.arguments.iter().map(Argument::default)).tokenize()), )), ); - out.write(("impl", name, block((fn_props, fn_render)))); + mo.write((fn_props, fn_render)); let field_generics = ('<', each(self.arguments.iter().map(Argument::name)), '>').tokenize(); - out.write(( + mo.write(( "#[allow(non_camel_case_types)] impl", field_generics.clone(), - name, + "Props", field_generics, - block(each(self.arguments.iter().enumerate().map(|(i, a)| { - a.setter(name, finder.as_mut(), i, &self.arguments) - }))), + block(each( + self.arguments + .iter() + .enumerate() + .map(|(i, a)| a.setter(finder.as_mut(), i, &self.arguments)), + )), + )); + + // panic!("{mo}"); + + out.write((&self.r#pub, self.r#fn, name, self.generics, self.raw_args)); + out.write((self.ret, block(self.render))); + + out.write(( + format!( + "#[doc = \"`#[component]` handlers for the [`{name}`](fn.{name}.html) function.\"]" + ) + .as_str(), + self.r#pub, + self.r#mod, + name, + block(("use super::*;", mo)), )); } } @@ -390,7 +404,6 @@ impl Argument { fn setter<'a>( &'a self, - comp: &'a Ident, finder: Option<&mut GenericFinder>, pos: usize, args: &'a [Argument], @@ -427,7 +440,7 @@ impl Argument { } } - let ret_type = ("->", comp, '<', ret_generics, '>', where_clause); + let ret_type = ("-> Props<", ret_generics, '>', where_clause); ( "#[inline(always)] pub fn ", @@ -442,7 +455,7 @@ impl Argument { ("self, value:", maybe_ty), ), ret_type, - block((comp, block(body))), + block(("Props", block(body))), ) } diff --git a/crates/kobold_macros/src/gen.rs b/crates/kobold_macros/src/gen.rs index 7793dad5..ea386340 100644 --- a/crates/kobold_macros/src/gen.rs +++ b/crates/kobold_macros/src/gen.rs @@ -20,7 +20,7 @@ mod transient; pub use element::JsElement; pub use fragment::{append, JsFragment}; pub use transient::{Anchor, Field, FieldKind, Hint, Transient}; -pub use transient::{JsArgument, JsFnName, JsFunction, JsModule, JsString}; +pub use transient::{JsArgument, JsFnName, JsFunction, JsString}; // Short string for auto-generated variable names pub type Short = ArrayString<4>; diff --git a/crates/kobold_macros/src/gen/component.rs b/crates/kobold_macros/src/gen/component.rs index 61b7d904..483edff6 100644 --- a/crates/kobold_macros/src/gen/component.rs +++ b/crates/kobold_macros/src/gen/component.rs @@ -12,11 +12,7 @@ impl Component { fn into_expression(self) -> TokenStream { let mut render = self.path.clone(); - render.write(if self.children.is_some() { - "::__render_with" - } else { - "::__render" - }); + render.write("::render"); if let Some(generics) = self.generics { render.write(("::", generics)); @@ -24,7 +20,7 @@ impl Component { let mut params = self.path; - params.write("::__undefined()"); + params.write("::props()"); for Property { name, expr } in self.props { params.write(('.', call(name, expr.stream))); @@ -33,7 +29,7 @@ impl Component { if let Some(children) = self.children { let children = crate::gen::generate(children); - params.write((',', children)); + params.write(('.', call("children", children))); } call(render, params) diff --git a/crates/kobold_macros/src/gen/transient.rs b/crates/kobold_macros/src/gen/transient.rs index 923739c4..2b19ac55 100644 --- a/crates/kobold_macros/src/gen/transient.rs +++ b/crates/kobold_macros/src/gen/transient.rs @@ -32,7 +32,7 @@ pub struct Hint { impl Transient { fn is_const(&self) -> bool { - let jsfn = match self.js.functions.get(0) { + let jsfn = match self.js.functions.first() { Some(fun) => fun, None => return false, }; diff --git a/crates/kobold_macros/src/parse.rs b/crates/kobold_macros/src/parse.rs index e941d213..99f7293f 100644 --- a/crates/kobold_macros/src/parse.rs +++ b/crates/kobold_macros/src/parse.rs @@ -94,7 +94,7 @@ impl Tokenize for ParseError { }) .collect::(); - ("fn _parse_error()", block(err)).tokenize_in(stream) + block(("fn _parse_error()", block(err), "0")).tokenize_in(stream) } } @@ -307,7 +307,7 @@ mod util { use arrayvec::ArrayString; thread_local! { - static FMT_BUF: RefCell> = RefCell::new(ArrayString::new()); + static FMT_BUF: RefCell> = const { RefCell::new(ArrayString::new_const()) }; } pub trait IdentExt: Display { diff --git a/crates/kobold_macros/src/syntax.rs b/crates/kobold_macros/src/syntax.rs index 4ba61811..52593b9a 100644 --- a/crates/kobold_macros/src/syntax.rs +++ b/crates/kobold_macros/src/syntax.rs @@ -13,6 +13,7 @@ use crate::tokenize::prelude::*; /// Regular Rust ``, we don't care about what they are, /// but we do care about nested angle braces. +#[derive(Clone)] pub struct Generics { pub tokens: TokenStream, } diff --git a/crates/kobold_qr/Cargo.toml b/crates/kobold_qr/Cargo.toml index 8eda615b..8c282b95 100644 --- a/crates/kobold_qr/Cargo.toml +++ b/crates/kobold_qr/Cargo.toml @@ -10,7 +10,7 @@ description = "QR code component for Kobold" [dependencies] fast_qr = "0.8.5" -kobold = { version = "0.9.0", path = "../kobold" } +kobold = { version = "0.10.0", path = "../kobold" } wasm-bindgen = "0.2.84" [dependencies.web-sys] diff --git a/crates/kobold_qr/src/lib.rs b/crates/kobold_qr/src/lib.rs index c3db6285..5df2d5df 100644 --- a/crates/kobold_qr/src/lib.rs +++ b/crates/kobold_qr/src/lib.rs @@ -10,7 +10,7 @@ use kobold::diff::fence; use web_sys::CanvasRenderingContext2d; #[component(size?: 200)] -pub fn KoboldQR(data: &str, size: usize) -> impl View + '_ { +pub fn qr(data: &str, size: usize) -> impl View + '_ { fence(data, move || { let qr = QRBuilder::new(data).build().ok()?; let pixel = ((size / qr.size) + 1) * 2; diff --git a/examples/counter/Trunk.toml b/examples/counter/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/counter/Trunk.toml +++ b/examples/counter/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 9ec684e7..3ae52c85 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,26 +1,23 @@ use kobold::prelude::*; -#[component] -fn Counter() -> impl View { - stateful(0_u32, |count| { - bind! { count: - let onclick = move |_| *count += 1; - let reset = move |_| *count = 0; - } +fn app(count: &Hook) -> impl View + '_ { + bind! { count: + let onclick = move |_| *count += 1; + let reset = move |_| *count = 0; + } - view! { -

          - + view! { +

          + - // `{onclick}` here is shorthand for `onclick={onclick}` - - - } - }) + // `{onclick}` here is shorthand for `onclick={onclick}` + + + } } #[component(auto_branch)] -fn ShowCount(count: u32) -> impl View { +fn counter(count: u32) -> impl View { let count = match count { 0 => view! { "zero times." }, 1 => view! { "once." }, @@ -31,7 +28,5 @@ fn ShowCount(count: u32) -> impl View { } fn main() { - kobold::start(view! { - - }); + kobold::start(stateful(0_u32, app)); } diff --git a/examples/csv_editor/Trunk.toml b/examples/csv_editor/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/csv_editor/Trunk.toml +++ b/examples/csv_editor/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/csv_editor/src/main.rs b/examples/csv_editor/src/main.rs index 6e9aeec0..89d20e53 100644 --- a/examples/csv_editor/src/main.rs +++ b/examples/csv_editor/src/main.rs @@ -7,7 +7,7 @@ mod state; use state::{Editing, State, Text}; #[component] -fn Editor() -> impl View { +fn editor() -> impl View { stateful(State::mock, |state| { let onload = state.bind_async(|state, event: Event| async move { let file = match event.target().files().and_then(|list| list.get(0)) { @@ -43,16 +43,14 @@ fn Editor() -> impl View { { - for state.columns().map(|col| view! { }) + for state.columns().map(|col| head(col, state)) } { for state.rows().map(move |row| view! { { - for state.columns().map(move |col| view! { - - }) + for state.columns().map(move |col| cell(col, row, state)) } }) } @@ -61,7 +59,7 @@ fn Editor() -> impl View { } #[component(auto_branch)] -fn Head(col: usize, state: &Hook) -> impl View + '_ { +fn head(col: usize, state: &Hook) -> impl View + '_ { let value = state.source.get_text(&state.columns[col]); if state.editing == (Editing::Column { col }) { @@ -83,7 +81,7 @@ fn Head(col: usize, state: &Hook) -> impl View + '_ { } #[component(auto_branch)] -fn Cell(col: usize, row: usize, state: &Hook) -> impl View + '_ { +fn cell(col: usize, row: usize, state: &Hook) -> impl View + '_ { let value = state.source.get_text(&state.rows[row][col]); if state.editing == (Editing::Cell { row, col }) { @@ -121,6 +119,6 @@ fn Cell(col: usize, row: usize, state: &Hook) -> impl View + '_ { fn main() { kobold::start(view! { - + }); } diff --git a/examples/hello_world/Trunk.toml b/examples/hello_world/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/hello_world/Trunk.toml +++ b/examples/hello_world/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 61618291..70eb0e39 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -1,13 +1,15 @@ use kobold::prelude::*; #[component] -fn Hello(name: &str) -> impl View + '_ { - // No need to close tags at the end of the macro - view! {

          "Hello "{ name }"!" } +fn hello(name: &str) -> impl View + '_ { + view! { + // No need to close tags at the end of the macro +

          "Hello "{ name }"!" + } } fn main() { kobold::start(view! { - + }); } diff --git a/examples/interval/Trunk.toml b/examples/interval/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/interval/Trunk.toml +++ b/examples/interval/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/interval/src/main.rs b/examples/interval/src/main.rs index 8703bd30..380f9d40 100644 --- a/examples/interval/src/main.rs +++ b/examples/interval/src/main.rs @@ -2,7 +2,7 @@ use gloo_timers::callback::Interval; use kobold::prelude::*; #[component] -fn Elapsed(seconds: u32) -> impl View { +fn elapsed(seconds: u32) -> impl View { stateful(seconds, |seconds| { bind! { seconds: @@ -29,6 +29,6 @@ fn Elapsed(seconds: u32) -> impl View { fn main() { kobold::start(view! { - + }); } diff --git a/examples/list/Trunk.toml b/examples/list/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/list/Trunk.toml +++ b/examples/list/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/list/src/main.rs b/examples/list/src/main.rs index e82e1ae0..d0c34675 100644 --- a/examples/list/src/main.rs +++ b/examples/list/src/main.rs @@ -1,7 +1,7 @@ use kobold::prelude::*; #[component] -fn ListExample(count: u32) -> impl View { +fn list_example(count: u32) -> impl View { stateful(count, |count| { bind! { count: let dec = move |_| *count = count.saturating_sub(1); @@ -23,21 +23,19 @@ fn ListExample(count: u32) -> impl View { // // On subsequent renders `Kobold` can very cheaply diff items yielded // by iterators, avoiding allocations unless new items are added. - // - // `{n}` is just shorthand for `n={n}`. - for (1..=count.get()).map(|n| view! { }) + for (1..=count.get()).map(list_item) } } }) } #[component] -fn ListItem(n: u32) -> impl View { +fn list_item(n: u32) -> impl View { view! {
        • "Item #"{ n }
        • } } fn main() { kobold::start(view! { - + }); } diff --git a/examples/optional_params/Trunk.toml b/examples/optional_params/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/optional_params/Trunk.toml +++ b/examples/optional_params/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/optional_params/src/main.rs b/examples/optional_params/src/main.rs index f0820331..00d8e676 100644 --- a/examples/optional_params/src/main.rs +++ b/examples/optional_params/src/main.rs @@ -6,7 +6,7 @@ use kobold::prelude::*; // Make `age` an optional parameter, use the `Default` value age?, )] -fn Greeter<'a>(name: &'a str, age: Option) -> impl View + 'a { +fn greeter<'a>(name: &'a str, age: Option) -> impl View + 'a { let age = age.map(|age| view!(", you are "{ age }" years old")); view! { @@ -17,10 +17,10 @@ fn Greeter<'a>(name: &'a str, age: Option) -> impl View + 'a { fn main() { kobold::start(view! { // Hello Kobold - + // Hello Alice - + // Hello Bob, you are 42 years old - + }); } diff --git a/examples/qrcode/Trunk.toml b/examples/qrcode/Trunk.toml index b35ea20f..c629e4f0 100644 --- a/examples/qrcode/Trunk.toml +++ b/examples/qrcode/Trunk.toml @@ -3,7 +3,6 @@ wasm_bindgen = "0.2.84" [build] filehash = false -pattern_script = "" # Does not work with `trunk serve` # public_url = "./" diff --git a/examples/qrcode/src/main.rs b/examples/qrcode/src/main.rs index c2cb9558..d19a6944 100644 --- a/examples/qrcode/src/main.rs +++ b/examples/qrcode/src/main.rs @@ -1,9 +1,9 @@ use kobold::prelude::*; use kobold::reexport::web_sys::HtmlTextAreaElement; -use kobold_qr::KoboldQR; +use kobold_qr::qr; #[component] -fn QRExample() -> impl View { +fn qr_example() -> impl View { stateful("Enter something", |data| { bind! { data: @@ -13,14 +13,12 @@ fn QRExample() -> impl View { view! {

          "QR code example"

          - + } }) } fn main() { - kobold::start(view! { - - }); + kobold::start(qr_example()); } diff --git a/examples/stateful/Trunk.toml b/examples/stateful/Trunk.toml index 48c92068..e46336a9 100644 --- a/examples/stateful/Trunk.toml +++ b/examples/stateful/Trunk.toml @@ -1,5 +1,2 @@ [tools] wasm_bindgen = "0.2.84" - -[build] -pattern_script = "" diff --git a/examples/stateful/src/main.rs b/examples/stateful/src/main.rs index 328036f7..41e5e845 100644 --- a/examples/stateful/src/main.rs +++ b/examples/stateful/src/main.rs @@ -15,41 +15,36 @@ impl State { } } -#[component] -fn App() -> impl View { - stateful(State::new, |state| { - bind! { state: - // Since we work with a state that owns a `String`, - // callbacks can mutate it at will. - let exclaim = move |_| state.name.push('!'); +fn app(state: &Hook) -> impl View + '_ { + bind! { state: + // Since we work with a state that owns a `String`, + // callbacks can mutate it at will. + let exclaim = move |_| state.name.push('!'); - // Repeatedly clicking the Alice button does not have to do anything. - let alice = move |_| if state.name != "Alice" { - "Alice".clone_into(&mut state.name); - Then::Render - } else { - Then::Stop - }; + // Repeatedly clicking the Alice button does not have to do anything. + let alice = move |_| if state.name != "Alice" { + "Alice".clone_into(&mut state.name); + Then::Render + } else { + Then::Stop + }; - let inc_age = move |_| state.age += 1; - let adult = move |_| state.age = 18; - } + let inc_age = move |_| state.age += 1; + let adult = move |_| state.age = 18; + } - view! { -
          - // Render can borrow `name` from state, no need for clones -

          { &state.name }" is "{ state.age }" years old."

          - - - " " - - - } - }) + view! { +
          + // Render can borrow `name` from state, no need for clones +

          { &state.name }" is "{ state.age }" years old."

          + + + " " + + + } } fn main() { - kobold::start(view! { - - }); + kobold::start(stateful(State::new, app)); } diff --git a/examples/todomvc/Trunk.toml b/examples/todomvc/Trunk.toml index b35ea20f..c629e4f0 100644 --- a/examples/todomvc/Trunk.toml +++ b/examples/todomvc/Trunk.toml @@ -3,7 +3,6 @@ wasm_bindgen = "0.2.84" [build] filehash = false -pattern_script = "" # Does not work with `trunk serve` # public_url = "./" diff --git a/examples/todomvc/src/filter.rs b/examples/todomvc/src/filter.rs deleted file mode 100644 index 8dc91407..00000000 --- a/examples/todomvc/src/filter.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Filter { - All, - Active, - Completed, -} - -impl Filter { - pub fn href(self) -> &'static str { - match self { - Filter::All => "#/", - Filter::Active => "#/active", - Filter::Completed => "#/completed", - } - } - - pub fn label(self) -> &'static str { - match self { - Filter::All => "All", - Filter::Active => "Active", - Filter::Completed => "Completed", - } - } -} diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index fbf08b4a..9d1bc45d 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -1,67 +1,62 @@ use kobold::prelude::*; use web_sys::HtmlInputElement as InputElement; -mod filter; mod state; -use filter::Filter; use state::*; -#[component] -fn App() -> impl View { - stateful(State::default, |state| { - let hidden = class!("hidden" if state.entries.is_empty()); +fn app(state: &Hook) -> impl View + '_ { + let hidden = class!("hidden" if state.entries.is_empty()); - let active_count = state.count_active(); - let completed_hidden = class!("hidden" if state.entries.len() == active_count); + let active_count = state.count_active(); + let completed_hidden = class!("hidden" if state.entries.len() == active_count); - bind! { state: - let clear = move |_| state.clear(); - } + bind! { state: + let clear = move |_| state.clear(); + } - view! { - - - -

          "todos"

          - - -
          - - - { - for state - .filtered_entries() - .map(move |(idx, entry)| view! { }) - } -
          - - - { active_count } - { - ref match active_count { - 1 => " item left", - _ => " items left", - } - } - - - - - -

        - "Clear completed" + view! { + + + +

        "todos"

        + + +
        + + + { + for state + .filtered_entries() + .map(move |(idx, entry)| view! { }) + }
        - -

        "Double-click to edit a todo" -

        "Written by ""Maciej Hirsz" -

        "Part of ""TodoMVC" - } - }) + + + { active_count } + { + ref match active_count { + 1 => " item left", + _ => " items left", + } + } + + + + + +

      + "Clear completed" + + +

      "Double-click to edit a todo" +

      "Written by ""Maciej Hirsz" +

      "Part of ""TodoMVC" + } } #[component] -fn EntryInput(state: &Hook) -> impl View + '_ { +fn entry_input(state: &Hook) -> impl View + '_ { bind! { state: @@ -80,19 +75,19 @@ fn EntryInput(state: &Hook) -> impl View + '_ { } #[component] -fn ToggleAll(active_count: usize, state: &Hook) -> impl View + '_ { +fn toggle_all(active_count: usize, state: &Hook) -> impl View + '_ { bind! { state: let onclick = move |_| state.set_all(active_count != 0); } view! { -

    • { static filter.label() } +
    • + + { static by.label() } } } fn main() { - kobold::start(view! { - - }); + kobold::start(stateful(State::default, app)); } diff --git a/examples/todomvc/src/state.rs b/examples/todomvc/src/state.rs index 5b6cdb31..4d324076 100644 --- a/examples/todomvc/src/state.rs +++ b/examples/todomvc/src/state.rs @@ -1,8 +1,6 @@ use gloo_storage::{LocalStorage, Storage}; use wasm_bindgen::UnwrapThrowExt; -use crate::filter::Filter; - const KEY: &str = "kobold.todomvc.example"; pub struct State { @@ -160,3 +158,28 @@ impl State { self.store(); } } + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Filter { + pub fn href(self) -> &'static str { + match self { + Filter::All => "#/", + Filter::Active => "#/active", + Filter::Completed => "#/completed", + } + } + + pub fn label(self) -> &'static str { + match self { + Filter::All => "All", + Filter::Active => "Active", + Filter::Completed => "Completed", + } + } +}