yo-html 0.1.1

JSX-like macro similar to what you can find in React or Yew but without framework nor trait.
Documentation
pub mod yo_web_framework_example;

#[xtask_wasm::run_example]
fn run_app() {
    use yo_web_framework_example::prelude::*;

    // Item in the groceries list

    #[derive(Default)]
    struct ItemBuilder {
        key: Option<Key>,
        onchange: Option<Box<dyn Fn((IString, u32))>>,
    }

    impl ItemBuilder {
        pub fn set_attr_key(&mut self, key: impl std::hash::Hash) -> &mut Self {
            use std::hash::Hasher;
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
            key.hash(&mut hasher);
            self.key.replace(hasher.finish().into());
            self
        }
        fn set_attr_onchange(&mut self, f: impl Fn((IString, u32)) + 'static) -> &mut Self {
            self.onchange.replace(Box::new(f));
            self
        }
        fn finish(&mut self) -> VNode {
            VNode::from(VNodeStatefulComponent::new(Item {
                key: self.key,
                onchange: self.onchange.take().map(Rc::from),
            }))
        }
    }

    #[derive(Clone)]
    struct Item {
        key: Option<Key>,
        onchange: Option<Rc<dyn Fn((IString, u32))>>,
    }

    impl Item {
        fn builder(_tag: &'static str) -> ItemBuilder {
            Default::default()
        }
    }

    impl StatefulComponent for Item {
        fn key(&self) -> Option<Key> {
            self.key
        }
        fn update(&mut self, _other: Self) -> bool {
            false
        }
        fn render(&self, ctx: &StatefulComponentHandler<Self>) -> VNode {
            let (name, count) =
                ctx.with_state(|state: &ItemState| (state.name.clone(), state.count));
            let inc = {
                let onchange = self.onchange.clone();
                ctx.callback(move |_, state: &mut ItemState, _ev: web_sys::Event| {
                    state.count = state.count.saturating_add(1);
                    if let Some(onchange) = onchange.clone() {
                        (onchange)((state.name.clone(), state.count));
                    }
                })
            };
            let dec = {
                let onchange = self.onchange.clone();
                ctx.callback(move |_, state: &mut ItemState, _ev: web_sys::Event| {
                    state.count = state.count.saturating_sub(1);
                    if let Some(onchange) = onchange.clone() {
                        (onchange)((state.name.clone(), state.count));
                    }
                })
            };
            let onchange = {
                let onchange = self.onchange.clone();
                ctx.callback(move |_, state: &mut ItemState, event: web_sys::Event| {
                    let Some(input) = event
                        .target()
                        .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
                    else {
                        return;
                    };
                    state.name = input.value().into();
                    if let Some(onchange) = onchange.clone() {
                        (onchange)((state.name.clone(), state.count));
                    }
                })
            };

            html! {
                <>
                    <input {onchange}>{name}</input>
                    {" ("}("{}", count){")"}
                    <button onclick={dec}>{"-"}</button>
                    <button onclick={inc}>{"+"}</button>
                </>
            }
        }
    }

    #[derive(Default)]
    struct ItemState {
        name: IString,
        count: u32,
    }

    // The app "groceries"

    struct AppBuilder;

    impl AppBuilder {
        fn finish(&mut self) -> VNode {
            VNode::from(VNodeStatefulComponent::new(App))
        }
    }

    #[derive(Clone)]
    struct App;

    impl App {
        fn builder(_tag: &'static str) -> AppBuilder {
            AppBuilder
        }
    }

    impl StatefulComponent for App {
        fn update(&mut self, _other: Self) -> bool {
            false
        }
        fn render(&self, ctx: &StatefulComponentHandler<Self>) -> VNode {
            let add_item = ctx.callback(|_, state: &mut AppState, _ev: web_sys::Event| {
                state.list.push(Default::default());
            });
            let list = ctx.with_state(|state: &AppState| {
                state
                    .list
                    .iter()
                    .enumerate()
                    .map(|(i, _)| {
                        let ctx = ctx.clone();
                        let onchange = move |(new_name, new_count): (IString, u32)| {
                            ctx.with_state_mut(|state: &mut AppState| {
                                if let Some((name, count)) = state.list.get_mut(i) {
                                    *name = new_name;
                                    *count = new_count;
                                    log!("Item #{i}: {name} ({count})");
                                }
                            });
                        };
                        html! {
                            <li key={i}><Item {onchange} /></li>
                        }
                    })
                    .collect::<VNode>()
            });

            html! {
                <>
                    <h1>{"Groceries"}</h1>
                    <ul>
                        {list}
                    </ul>
                    <button onclick={add_item}>{"Add item"}</button>
                </>
            }
        }
    }

    #[derive(Default)]
    struct AppState {
        list: Vec<(IString, u32)>,
    }

    // startup

    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();
    let _ = html!(<App />).create_dom(&body);
}