diff --git a/Cargo.lock b/Cargo.lock index c44f7e031..efa262ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arrayref" version = "0.3.8" @@ -1068,10 +1074,28 @@ version = "0.1.0" dependencies = [ "console_error_panic_hook", "console_log", - "gloo-net", + "gloo-net 0.6.0", + "log", + "serde", + "wasm-bindgen", + "web-sys", + "xilem_web", +] + +[[package]] +name = "fetch_event_handler" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "console_log", + "futures", + "gloo-net 0.5.0", "log", "serde", + "thiserror", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", "xilem_web", ] @@ -1351,6 +1375,23 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "gloo-utils", + "http 0.2.12", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -1358,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" dependencies = [ "gloo-utils", - "http", + "http 1.1.0", "js-sys", "serde", "serde_json", @@ -1525,6 +1566,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8beb81d31..0e5ead6c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "xilem_web/web_examples/counter_custom_element", "xilem_web/web_examples/elm", "xilem_web/web_examples/fetch", + "xilem_web/web_examples/fetch_event_handler", "xilem_web/web_examples/todomvc", "xilem_web/web_examples/mathml_svg", "xilem_web/web_examples/raw_dom_access", diff --git a/xilem_web/src/event_handler.rs b/xilem_web/src/event_handler.rs new file mode 100644 index 000000000..484901b22 --- /dev/null +++ b/xilem_web/src/event_handler.rs @@ -0,0 +1,167 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{future::Future, marker::PhantomData, rc::Rc}; + +use wasm_bindgen::UnwrapThrowExt; +use wasm_bindgen_futures::spawn_local; +use xilem_core::{MessageResult, ViewPathTracker}; + +use crate::{context::MessageThunk, DynMessage, Message, ViewCtx}; + +pub enum EventHandlerMessage { + Event(E), + Message(Message), +} + +pub trait EventHandler: + 'static +{ + /// State that is used over the lifetime of the retained representation of the event handler. + /// + /// This often means routing information for messages to child event handlers or state for async handlers, + type State; + + /// Init and create the corresponding state. + fn build(&self, ctx: &mut Context) -> Self::State; + + /// Update handler state based on the difference between `self` and `prev`. + fn rebuild(&self, prev: &Self, event_handler_state: &mut Self::State, ctx: &mut Context); + + /// Cleanup the handler, when it's being removed from the tree. + /// + /// The main use-cases of this method are to: + /// - Cancel any async tasks + /// - Clean up any book-keeping set-up in `build` and `rebuild` + // TODO: Should this take ownership of the `EventHandlerState` + // We have chosen not to because it makes swapping versions more awkward + fn teardown(&self, event_handler_state: &mut Self::State, ctx: &mut Context); + + /// Route `message` to `id_path`, if that is still a valid path. + fn message( + &self, + event_handler_state: &mut Self::State, + id_path: &[xilem_core::ViewId], + message: EventHandlerMessage, + app_state: &mut State, + ) -> MessageResult>; +} + +// Because of intersecting trait impls with the blanket impl below, the following impl is unfortunately not possible: +// +// `impl Action> EventHandler<(), State, Action, ViewCtx> for F {}` +// +// A workaround for this would be to "hardcode" event types, instead of using a blanket impl. +// This is fortunately not a big issue in xilem_web, because there's AFAIK always an event payload (i.e. something different than `()`) + +impl EventHandler + for F +where + Context: ViewPathTracker, + F: Fn(&mut State, Event) -> Action + 'static, +{ + type State = (); + + fn build(&self, _ctx: &mut Context) -> Self::State {} + + fn rebuild(&self, _prev: &Self, _event_handler_state: &mut Self::State, _ctx: &mut Context) {} + + fn teardown(&self, _event_handler_state: &mut Self::State, _ctx: &mut Context) {} + + fn message( + &self, + _event_handler_state: &mut Self::State, + id_path: &[xilem_core::ViewId], + message: EventHandlerMessage, + app_state: &mut State, + ) -> MessageResult> { + debug_assert!(id_path.is_empty()); + match message { + EventHandlerMessage::Event(event) => MessageResult::Action(self(app_state, event)), + EventHandlerMessage::Message(_) => unreachable!(), + } + } +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct DeferEventHandler { + #[allow(clippy::complexity)] + phantom: PhantomData (State, Action, FOut, F)>, + future_fn: FF, + callback_fn: CF, +} + +impl EventHandler + for DeferEventHandler +where + State: 'static, + Action: 'static, + Event: 'static, + FOut: Message, + F: Future + 'static, + FF: Fn(&mut State, Event) -> F + 'static, + CF: Fn(&mut State, FOut) -> Action + 'static, +{ + type State = Rc; + + fn build(&self, ctx: &mut ViewCtx) -> Self::State { + Rc::new(ctx.message_thunk()) + } + + fn rebuild(&self, _prev: &Self, _event_handler_state: &mut Self::State, _ctx: &mut ViewCtx) {} + + fn teardown(&self, _event_handler_state: &mut Self::State, _ctx: &mut ViewCtx) {} + + fn message( + &self, + message_thunk: &mut Self::State, + id_path: &[xilem_core::ViewId], + message: EventHandlerMessage, + app_state: &mut State, + ) -> MessageResult> { + debug_assert!(id_path.is_empty()); + match message { + EventHandlerMessage::Event(event) => { + let future = (self.future_fn)(app_state, event); + let thunk = Rc::clone(message_thunk); + // TODO currently, multiple events could trigger this, while the (old) future is still not resolved + // This may be intended, but can also lead to surprising behavior. + // We could add an atomic boolean, to avoid this, i.e. either block a new future, + // or even queue it after the first future being resolved, there may also be other possible desired behaviors + // This could also be made configurable, e.g. via the builder pattern, like this: + // ``` + // defer(...) + // .block() // block new events triggering that future + // .once() // allow this event to trigger the future only once. + // .queue() // queue additional triggered futures + // ``` + spawn_local(async move { thunk.push_message(future.await) }); + MessageResult::RequestRebuild + } + EventHandlerMessage::Message(output) => MessageResult::Action((self.callback_fn)( + app_state, + *output.downcast::().unwrap_throw(), + )), + } + } +} + +pub fn defer( + future_fn: FF, + callback_fn: CF, +) -> DeferEventHandler +where + State: 'static, + Action: 'static, + Event: 'static, + FOut: Message, + F: Future + 'static, + FF: Fn(&mut State, Event) -> F + 'static, + CF: Fn(&mut State, FOut) -> Action + 'static, +{ + DeferEventHandler { + phantom: PhantomData, + future_fn, + callback_fn, + } +} diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 0f0f1525c..25ffcae69 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -6,31 +6,35 @@ use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::{js_sys, AddEventListenerOptions}; use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; -use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx}; +use crate::{ + event_handler::{EventHandler, EventHandlerMessage}, + DynMessage, ElementAsRef, OptionalAction, ViewCtx, +}; /// Use a distinctive number here, to be able to catch bugs. /// In case the generational-id view path in `View::Message` lead to a wrong view const ON_EVENT_VIEW_ID: ViewId = ViewId::new(0x2357_1113); +const EVENT_HANDLER_ID: ViewId = ViewId::new(0x2357_1114); /// Wraps a [`View`] `V` and attaches an event listener. /// /// The event type `Event` should inherit from [`web_sys::Event`] #[derive(Clone, Debug)] -pub struct OnEvent { +pub struct OnEvent OA> { pub(crate) element: V, pub(crate) event: Cow<'static, str>, pub(crate) capture: bool, pub(crate) passive: bool, - pub(crate) handler: Callback, + pub(crate) handler: Handler, #[allow(clippy::type_complexity)] - pub(crate) phantom_event_ty: PhantomData (State, Action, Event)>, + pub(crate) phantom_event_ty: PhantomData (State, Action, OA, Event)>, } -impl OnEvent +impl OnEvent where Event: JsCast + 'static, { - pub fn new(element: V, event: impl Into>, handler: Callback) -> Self { + pub fn new(element: V, event: impl Into>, handler: Handler) -> Self { OnEvent { element, event: event.into(), @@ -108,61 +112,74 @@ fn remove_event_listener( } /// State for the `OnEvent` view. -pub struct OnEventState { +pub struct OnEventState { #[allow(unused)] - child_state: S, + child_state: CS, + handler_state: HS, callback: Closure, } // These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below. -fn build_event_listener( +fn build_event_listener( element_view: &V, + event_handler: &Handler, event: &str, capture: bool, passive: bool, ctx: &mut ViewCtx, -) -> (V::Element, OnEventState) +) -> (V::Element, OnEventState) where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, + Handler: EventHandler, { - // we use a placeholder id here, the id can never change, so we don't need to store it anywhere - ctx.with_id(ON_EVENT_VIEW_ID, |ctx| { + let handler_state = ctx.with_id(EVENT_HANDLER_ID, |ctx| event_handler.build(ctx)); + let (element, (child_state, callback)) = ctx.with_id(ON_EVENT_VIEW_ID, |ctx| { let (element, child_state) = element_view.build(ctx); let callback = create_event_listener::(element.as_ref(), event, capture, passive, ctx); - let state = OnEventState { - child_state, - callback, - }; - (element, state) - }) + (element, (child_state, callback)) + }); + let state = OnEventState { + child_state, + handler_state, + callback, + }; + (element, state) } #[allow(clippy::too_many_arguments)] -fn rebuild_event_listener<'el, State, Action, V, Event>( +fn rebuild_event_listener<'el, State, Action, OA, Handler, V, Event>( element_view: &V, prev_element_view: &V, + event_handler: &Handler, + prev_event_handler: &Handler, element: Mut<'el, V::Element>, event: &str, capture: bool, passive: bool, prev_capture: bool, prev_passive: bool, - state: &mut OnEventState, + state: &mut OnEventState, ctx: &mut ViewCtx, ) -> Mut<'el, V::Element> where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, + Handler: EventHandler, { + ctx.with_id(EVENT_HANDLER_ID, |ctx| { + event_handler.rebuild(prev_event_handler, &mut state.handler_state, ctx); + }); ctx.with_id(ON_EVENT_VIEW_ID, |ctx| { if prev_capture != capture || prev_passive != passive { remove_event_listener(element.as_ref(), event, &state.callback, prev_capture); @@ -174,19 +191,25 @@ where }) } -fn teardown_event_listener( +fn teardown_event_listener( element_view: &V, + event_handler: &Handler, element: Mut<'_, V::Element>, _event: &str, - state: &mut OnEventState, + state: &mut OnEventState, _capture: bool, ctx: &mut ViewCtx, ) where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, + Handler: EventHandler, { + ctx.with_id(EVENT_HANDLER_ID, |ctx| { + event_handler.teardown(&mut state.handler_state, ctx); + }); // TODO: is this really needed (as the element will be removed anyway)? // remove_event_listener(element.as_ref(), event, &state.callback, capture); ctx.with_id(ON_EVENT_VIEW_ID, |ctx| { @@ -194,13 +217,13 @@ fn teardown_event_listener( }); } -fn message_event_listener( +fn message_event_listener( element_view: &V, - state: &mut OnEventState, + handler: &Handler, + state: &mut OnEventState, id_path: &[ViewId], message: DynMessage, app_state: &mut State, - handler: &Callback, ) -> MessageResult where State: 'static, @@ -209,44 +232,62 @@ where V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA + 'static, + Handler: EventHandler + 'static, { let Some((first, remainder)) = id_path.split_first() else { throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); }; - if *first != ON_EVENT_VIEW_ID { - throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); - } - if remainder.is_empty() { - let event = message.downcast::().unwrap_throw(); - match (handler)(app_state, *event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, + let handler_message = if *first == EVENT_HANDLER_ID { + EventHandlerMessage::Message(message) + } else if *first == ON_EVENT_VIEW_ID { + if remainder.is_empty() { + EventHandlerMessage::Event(*message.downcast::().unwrap_throw()) + } else { + return element_view.message(&mut state.child_state, remainder, message, app_state); } } else { - element_view.message(&mut state.child_state, remainder, message, app_state) + throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); + }; + + match handler + .message(&mut state.handler_state, &[], handler_message, app_state) + .map(|action| action.action()) + { + MessageResult::Action(Some(action)) => MessageResult::Action(action), + MessageResult::Nop | MessageResult::Action(None) => MessageResult::Nop, + MessageResult::RequestRebuild => MessageResult::RequestRebuild, + MessageResult::Stale(EventHandlerMessage::Event(event)) => { + MessageResult::Stale(Box::new(event)) + } + MessageResult::Stale(EventHandlerMessage::Message(message)) => { + MessageResult::Stale(message) + } } } -impl ViewMarker for OnEvent {} -impl View - for OnEvent +impl ViewMarker + for OnEvent +{ +} +impl View + for OnEvent where State: 'static, Action: 'static, - OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA + 'static, + OA: OptionalAction + 'static, + Handler: EventHandler, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, { - type ViewState = OnEventState; + type ViewState = OnEventState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - build_event_listener::<_, _, _, Event>( + build_event_listener::<_, _, _, _, _, Event>( &self.element, + &self.handler, &self.event, self.capture, self.passive, @@ -261,6 +302,10 @@ where ctx: &mut ViewCtx, element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { + ctx.with_id(EVENT_HANDLER_ID, |ctx| { + self.handler + .rebuild(&prev.handler, &mut view_state.handler_state, ctx); + }); // special case, where event name can change, so we can't reuse the rebuild_event_listener function above ctx.with_id(ON_EVENT_VIEW_ID, |ctx| { if prev.capture != self.capture @@ -295,6 +340,7 @@ where ) { teardown_event_listener( &self.element, + &self.handler, element, &self.event, view_state, @@ -312,11 +358,11 @@ where ) -> MessageResult { message_event_listener( &self.element, + &self.handler, view_state, id_path, message, app_state, - &self.handler, ) } } @@ -324,17 +370,17 @@ where macro_rules! event_definitions { ($(($ty_name:ident, $event_name:literal, $web_sys_ty:ident)),*) => { $( - pub struct $ty_name { + pub struct $ty_name { pub(crate) element: V, pub(crate) capture: bool, pub(crate) passive: bool, - pub(crate) handler: Callback, - pub(crate) phantom_event_ty: PhantomData (State, Action)>, + pub(crate) handler: Handler, + pub(crate) phantom_event_ty: PhantomData (State, Action, OA)>, } - impl ViewMarker for $ty_name {} - impl $ty_name { - pub fn new(element: V, handler: Callback) -> Self { + impl ViewMarker for $ty_name {} + impl $ty_name { + pub fn new(element: V, handler: Handler) -> Self { Self { element, passive: true, @@ -368,23 +414,24 @@ macro_rules! event_definitions { } - impl View - for $ty_name + impl View + for $ty_name where State: 'static, Action: 'static, - OA: OptionalAction, - Callback: Fn(&mut State, web_sys::$web_sys_ty) -> OA + 'static, + OA: OptionalAction + 'static, + Handler: EventHandler, V: View, V::Element: ElementAsRef, { - type ViewState = OnEventState; + type ViewState = OnEventState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - build_event_listener::<_, _, _, web_sys::$web_sys_ty>( + build_event_listener::<_, _, _, _, _, web_sys::$web_sys_ty>( &self.element, + &self.handler, $event_name, self.capture, self.passive, @@ -399,9 +446,11 @@ macro_rules! event_definitions { ctx: &mut ViewCtx, element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { - rebuild_event_listener::<_, _, _, web_sys::$web_sys_ty>( + rebuild_event_listener::<_, _, _, _, _, web_sys::$web_sys_ty>( &self.element, &prev.element, + &self.handler, + &prev.handler, element, $event_name, self.capture, @@ -419,7 +468,7 @@ macro_rules! event_definitions { ctx: &mut ViewCtx, element: Mut<'_, Self::Element>, ) { - teardown_event_listener(&self.element, element, $event_name, view_state, self.capture, ctx); + teardown_event_listener(&self.element, &self.handler, element, $event_name, view_state, self.capture, ctx); } fn message( @@ -429,7 +478,7 @@ macro_rules! event_definitions { message: crate::DynMessage, app_state: &mut State, ) -> MessageResult { - message_event_listener(&self.element, view_state, id_path, message, app_state, &self.handler) + message_event_listener(&self.element, &self.handler, view_state, id_path, message, app_state) } } )* diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index ae00a5de8..eec9149ca 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -16,9 +16,10 @@ use std::borrow::Cow; use crate::{ attribute::{Attr, WithAttributes}, class::{AsClassIter, Class, WithClasses}, + event_handler::EventHandler, events, style::{IntoStyles, Style, WithStyle}, - DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, + DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, ViewCtx, }; use wasm_bindgen::JsCast; @@ -32,15 +33,15 @@ macro_rules! event_handler_mixin { // We *could* add another parameter to the macro to fix this, or probably even not provide these events directly on the `Element` interface // /// // #[doc = concat!("See for more details")] - fn $fn_name( + fn $fn_name( self, - handler: Callback, - ) -> events::$event_ty + handler: Handler, + ) -> events::$event_ty where Self: Sized, Self::Element: AsRef, OA: OptionalAction, - Callback: Fn(&mut State, web_sys::$web_sys_event_type) -> OA, + Handler: EventHandler, { events::$event_ty::new(self, handler) } @@ -108,28 +109,28 @@ pub trait Element: /// ``` /// use xilem_web::{interfaces::Element, elements::html::div}; /// # fn component() -> impl Element<()> { - /// div(()).on("custom-event", |state, event: web_sys::Event| {/* modify `state` */}) + /// div(()).on("custom-event", |state: &mut _, event: web_sys::Event| {/* modify `state` */}) /// # } /// ``` - fn on( + fn on( self, event: impl Into>, - handler: Callback, - ) -> events::OnEvent + handler: Handler, + ) -> events::OnEvent where Self::Element: AsRef, Event: JsCast + 'static, OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA, + Handler: EventHandler, Self: Sized, { events::OnEvent::new(self, event, handler) } - fn pointer( + fn pointer( self, - handler: Callback, - ) -> Pointer { + handler: Handler, + ) -> Pointer { crate::pointer::pointer(self, handler) } diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 40f2aa0f5..8f3ecbcc8 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -34,6 +34,7 @@ mod attribute_value; mod class; mod context; mod element_props; +pub mod event_handler; mod events; mod message; mod one_of; diff --git a/xilem_web/web_examples/elm/src/main.rs b/xilem_web/web_examples/elm/src/main.rs index 116c5efcb..aabf8ce5b 100644 --- a/xilem_web/web_examples/elm/src/main.rs +++ b/xilem_web/web_examples/elm/src/main.rs @@ -45,8 +45,8 @@ fn app_logic(model: &mut Model) -> impl HtmlDivElement { fn counter_view(count: i32) -> impl HtmlDivElement { el::div(( el::label(format!("count: {count}")), - el::button("+").on_click(|_, _| Message::Increment), - el::button("-").on_click(|_, _| Message::Decrement), + el::button("+").on_click(|_: &mut _, _| Message::Increment), + el::button("-").on_click(|_: &mut _, _| Message::Decrement), )) } diff --git a/xilem_web/web_examples/fetch_event_handler/Cargo.toml b/xilem_web/web_examples/fetch_event_handler/Cargo.toml new file mode 100644 index 000000000..6892a1659 --- /dev/null +++ b/xilem_web/web_examples/fetch_event_handler/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fetch_event_handler" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = "1.0.86" +console_error_panic_hook = "0.1" +console_log = "1" +futures = "0.3.30" +gloo-net = { version = "0.5.0", default-features = false, features = ["http", "json", "serde"] } +log = "0.4" +serde = { version = "1", features = ["derive"] } +thiserror = "1" +web-sys = { version = "0.3.69", features = ["Event", "HtmlInputElement"] } +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +xilem_web = { path = "../.." } diff --git a/xilem_web/web_examples/fetch_event_handler/index.html b/xilem_web/web_examples/fetch_event_handler/index.html new file mode 100644 index 000000000..271ad17fd --- /dev/null +++ b/xilem_web/web_examples/fetch_event_handler/index.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/xilem_web/web_examples/fetch_event_handler/src/main.rs b/xilem_web/web_examples/fetch_event_handler/src/main.rs new file mode 100644 index 000000000..dc08a0921 --- /dev/null +++ b/xilem_web/web_examples/fetch_event_handler/src/main.rs @@ -0,0 +1,112 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::{JsCast, UnwrapThrowExt as _}; +use xilem_web::{ + document_body, + elements::html::*, + event_handler::defer, + interfaces::{Element, HtmlDivElement, HtmlImageElement}, + App, +}; + +use gloo_net::http::Request; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Cat { + pub url: String, +} + +#[derive(Error, Clone, Debug)] +pub enum CatError { + #[error("Please request more than zero cats.")] + NonZeroCats, +} + +pub type CatCount = usize; + +pub async fn fetch_cats(count: CatCount) -> anyhow::Result> { + log::debug!("Fetch {count} cats"); + if count < 1 { + return Err(CatError::NonZeroCats.into()); + } + let url = format!("https://api.thecatapi.com/v1/images/search?limit={count}",); + Ok(Request::get(&url) + .send() + .await? + .json::>() + .await? + .into_iter() + .take(count) + .collect()) +} + +#[derive(Default)] +struct AppState { + cat_count: usize, + cats: Vec, + error: Option, +} + +pub fn event_target_value(event: &T) -> String +where + T: JsCast, +{ + event + .unchecked_ref::() + .target() + .unwrap_throw() + .unchecked_into::() + .value() +} + +fn app_logic(state: &mut AppState) -> impl HtmlDivElement { + let cats = state + .cats + .iter() + .map(|cat| p(img(()).src(cat.url.clone()))) + .collect::>(); + div(( + label(( + "How many cats would you like?", + input(()) + .attr("type", "number") + .attr("value", state.cat_count.to_string()) + .on_input(defer( + |state: &mut AppState, ev: web_sys::Event| { + let count = event_target_value(&ev).parse::().unwrap_or(0); + state.cat_count = count; + state.cats.clear(); + fetch_cats(count) + }, + |state: &mut AppState, fetch_result| match fetch_result { + Ok(cats) => { + log::info!("Received {} cats", cats.len()); + state.cats = cats; + state.error = None; + } + Err(err) => { + log::warn!("Unable to fetch cats: {err:#}"); + state.error = Some(err.to_string()); + } + }, + )), + )), + state + .error + .as_ref() + .map(|err| div((h2("Error"), p(err.to_string()))).class("error")), + cats, + )) +} + +pub fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + log::info!("Start application"); + + App::new(document_body(), AppState::default(), app_logic).run(); +} diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index 9ca24f1ae..179494ba9 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -58,7 +58,7 @@ fn app_logic(state: &mut AppState) -> impl DomView { .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) .collect::>(); svg(g(( - Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_, _| { + Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_: &mut _, _| { web_sys::console::log_1(&"app logic clicked".into()); }), Rect::new(210.0, 100.0, 310.0, 200.0) @@ -76,7 +76,7 @@ fn app_logic(state: &mut AppState) -> impl DomView { Color::YELLOW_GREEN, kurbo::Stroke::new(1.0).with_dashes(state.x, [7.0, 1.0]), ), - kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { + kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_: &mut _, _| { web_sys::console::log_1(&"circle clicked".into()); }), ))) diff --git a/xilem_web/web_examples/todomvc/src/main.rs b/xilem_web/web_examples/todomvc/src/main.rs index fe18459d7..152b45a2b 100644 --- a/xilem_web/web_examples/todomvc/src/main.rs +++ b/xilem_web/web_examples/todomvc/src/main.rs @@ -45,7 +45,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { el::input(()) .attr("value", todo.title_editing.clone()) .class("edit") - .on_keydown(|state: &mut Todo, evt| { + .on_keydown(|state: &mut Todo, evt: web_sys::KeyboardEvent| { let key = evt.key(); if key == "Enter" { state.save_editing(); @@ -56,7 +56,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { None } }) - .on_input(|state: &mut Todo, evt| { + .on_input(|state: &mut Todo, evt: web_sys::Event| { // TODO There could/should be further checks, if this is indeed the right event (same DOM element) if let Some(element) = evt .target() @@ -67,7 +67,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { } }) .passive(true) - .on_blur(|_, _| TodoAction::CancelEditing), + .on_blur(|_: &mut _, _| TodoAction::CancelEditing), )) .class(todo.completed.then_some("completed")) .class(editing.then_some("editing")) @@ -184,12 +184,12 @@ fn app_logic(state: &mut AppState) -> impl DomView { el::header(( el::h1("TODOs"), input - .on_keydown(|state: &mut AppState, evt| { + .on_keydown(|state: &mut AppState, evt: web_sys::KeyboardEvent| { if evt.key() == "Enter" { state.create_todo(); } }) - .on_input(|state: &mut AppState, evt| { + .on_input(|state: &mut AppState, evt: web_sys::Event| { // TODO There could/should be further checks, if this is indeed the right event (same DOM element) if let Some(element) = evt .target()