mirror of https://github.com/linebender/xilem
xilem_web: Add a `MemoizedAwait` view (#448)
This is the `rerun_on_change` view described in #440, I named it `MemoizedAwait` as I think that fits its functionality. It got also additional features `debounce_ms` (default `0`) and `reset_debounce_on_update` (default `true`) as I think that's quite useful, in case a lot of updates are happening. When `reset_debounce_on_update == false`, `debounce` is more a throttle than an actual debounce. This also adds a more heavily modified version of the example added in #427, also showing `OneOf` in xilem_web (which didn't have an example yet). This *could* be considered as alternative to #440 (at the very least the example) but those approaches could also well live next to each other. --------- Co-authored-by: Markus Kohlhase <markus.kohlhase@slowtec.de>
This commit is contained in:
parent
d70076262d
commit
981fcc4b5a
|
@ -1062,6 +1062,20 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fetch"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"gloo-net",
|
||||
"log",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"xilem_web",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
|
@ -1291,6 +1305,36 @@ 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",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.13.1"
|
||||
|
@ -1423,6 +1467,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 = "icu_collections"
|
||||
version = "1.5.0"
|
||||
|
@ -4069,6 +4124,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"peniko",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"xilem_core",
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ members = [
|
|||
"xilem_web/web_examples/counter",
|
||||
"xilem_web/web_examples/counter_custom_element",
|
||||
"xilem_web/web_examples/elm",
|
||||
"xilem_web/web_examples/fetch",
|
||||
"xilem_web/web_examples/todomvc",
|
||||
"xilem_web/web_examples/mathml_svg",
|
||||
"xilem_web/web_examples/svgtoy",
|
||||
|
|
|
@ -23,6 +23,7 @@ workspace = true
|
|||
xilem_core = { workspace = true, features = ["kurbo"] }
|
||||
peniko.workspace = true
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.69"
|
||||
|
|
|
@ -9,8 +9,11 @@ type CowStr = std::borrow::Cow<'static, str>;
|
|||
#[derive(PartialEq, Clone, Debug, PartialOrd)]
|
||||
pub enum AttributeValue {
|
||||
True, // for the boolean true, this serializes to an empty string (e.g. for <input checked>)
|
||||
I16(i16),
|
||||
U16(u16),
|
||||
I32(i32),
|
||||
U32(u32),
|
||||
Usize(usize),
|
||||
F32(f32),
|
||||
F64(f64),
|
||||
String(CowStr),
|
||||
|
@ -20,8 +23,11 @@ impl AttributeValue {
|
|||
pub fn serialize(&self) -> CowStr {
|
||||
match self {
|
||||
AttributeValue::True => "".into(), // empty string is equivalent to a true set attribute
|
||||
AttributeValue::I16(n) => n.to_string().into(),
|
||||
AttributeValue::U16(n) => n.to_string().into(),
|
||||
AttributeValue::I32(n) => n.to_string().into(),
|
||||
AttributeValue::U32(n) => n.to_string().into(),
|
||||
AttributeValue::Usize(n) => n.to_string().into(),
|
||||
AttributeValue::F32(n) => n.to_string().into(),
|
||||
AttributeValue::F64(n) => n.to_string().into(),
|
||||
AttributeValue::String(s) => s.clone(),
|
||||
|
@ -56,9 +62,15 @@ impl IntoAttributeValue for AttributeValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for u32 {
|
||||
impl IntoAttributeValue for i16 {
|
||||
fn into_attr_value(self) -> Option<AttributeValue> {
|
||||
Some(AttributeValue::U32(self))
|
||||
Some(AttributeValue::I16(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for u16 {
|
||||
fn into_attr_value(self) -> Option<AttributeValue> {
|
||||
Some(AttributeValue::U16(self))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +80,18 @@ impl IntoAttributeValue for i32 {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for u32 {
|
||||
fn into_attr_value(self) -> Option<AttributeValue> {
|
||||
Some(AttributeValue::U32(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for usize {
|
||||
fn into_attr_value(self) -> Option<AttributeValue> {
|
||||
Some(AttributeValue::Usize(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for f32 {
|
||||
fn into_attr_value(self) -> Option<AttributeValue> {
|
||||
Some(AttributeValue::F32(self))
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2024 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use xilem_core::{MessageResult, Mut, NoElement, View, ViewId, ViewPathTracker};
|
||||
|
||||
use crate::{DynMessage, OptionalAction, ViewCtx};
|
||||
|
||||
/// Await a future returned by `init_future` invoked with the argument `data`, `callback` is called with the output of the future. `init_future` will be invoked again, when `data` changes. Use [`memoized_await`] for construction of this [`View`]
|
||||
pub struct MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut> {
|
||||
init_future: InitFuture,
|
||||
data: Data,
|
||||
callback: Callback,
|
||||
debounce_ms: usize,
|
||||
reset_debounce_on_update: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
phantom: PhantomData<fn() -> (State, Action, OA, F, FOut)>,
|
||||
}
|
||||
|
||||
impl<State, Action, OA, InitFuture, Data, Callback, F, FOut>
|
||||
MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut>
|
||||
where
|
||||
FOut: std::fmt::Debug + 'static,
|
||||
F: Future<Output = FOut> + 'static,
|
||||
InitFuture: Fn(&Data) -> F,
|
||||
{
|
||||
/// Debounce the `init_future` function, when `data` updates,
|
||||
/// when `reset_debounce_on_update == false` then this throttles updates each `milliseconds`
|
||||
///
|
||||
/// The default for this is `0`
|
||||
pub fn debounce_ms(mut self, milliseconds: usize) -> Self {
|
||||
self.debounce_ms = milliseconds;
|
||||
self
|
||||
}
|
||||
|
||||
/// When `reset` is `true`, everytime `data` updates, the debounce timeout is cleared until `init_future` is invoked.
|
||||
/// This is only effective when `debounce > 0`
|
||||
///
|
||||
/// The default for this is `true`
|
||||
pub fn reset_debounce_on_update(mut self, reset: bool) -> Self {
|
||||
self.reset_debounce_on_update = reset;
|
||||
self
|
||||
}
|
||||
|
||||
fn init_future(&self, ctx: &mut ViewCtx, generation: u64) {
|
||||
ctx.with_id(ViewId::new(generation), |ctx| {
|
||||
let thunk = ctx.message_thunk();
|
||||
let future = (self.init_future)(&self.data);
|
||||
spawn_local(async move {
|
||||
thunk.push_message(MemoizedAwaitMessage::<FOut>::Output(future.await));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Await a future returned by `init_future` invoked with the argument `data`, `callback` is called with the output of the resolved future. `init_future` will be invoked again, when `data` changes.
|
||||
///
|
||||
/// The update behavior can be controlled, by [`debounce`](`MemoizedAwait::debounce`) and [`reset_debounce_on_update`](`MemoizedAwait::reset_debounce_on_update`)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use xilem_web::{core::fork, concurrent::memoized_await, elements::html::div, interfaces::Element};
|
||||
///
|
||||
/// fn app_logic(state: &mut i32) -> impl Element<i32> {
|
||||
/// fork(
|
||||
/// div(*state),
|
||||
/// memoized_await(
|
||||
/// 10,
|
||||
/// |count| std::future::ready(*count),
|
||||
/// |state, output| *state = output,
|
||||
/// )
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn memoized_await<State, Action, OA, InitFuture, Data, Callback, F, FOut>(
|
||||
data: Data,
|
||||
init_future: InitFuture,
|
||||
callback: Callback,
|
||||
) -> MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut>
|
||||
where
|
||||
State: 'static,
|
||||
Action: 'static,
|
||||
Data: PartialEq + 'static,
|
||||
FOut: std::fmt::Debug + 'static,
|
||||
F: Future<Output = FOut> + 'static,
|
||||
InitFuture: Fn(&Data) -> F + 'static,
|
||||
OA: OptionalAction<Action> + 'static,
|
||||
Callback: Fn(&mut State, FOut) -> OA + 'static,
|
||||
{
|
||||
MemoizedAwait {
|
||||
init_future,
|
||||
data,
|
||||
callback,
|
||||
debounce_ms: 0,
|
||||
reset_debounce_on_update: true,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemoizedAwaitState {
|
||||
generation: u64,
|
||||
schedule_update: bool,
|
||||
// Closures are retained so they can be called by environment
|
||||
schedule_update_fn: Option<Closure<dyn FnMut()>>,
|
||||
schedule_update_timeout_handle: Option<i32>,
|
||||
update: bool,
|
||||
}
|
||||
|
||||
impl MemoizedAwaitState {
|
||||
fn clear_update_timeout(&mut self) {
|
||||
if let Some(handle) = self.schedule_update_timeout_handle {
|
||||
web_sys::window()
|
||||
.unwrap_throw()
|
||||
.clear_timeout_with_handle(handle);
|
||||
}
|
||||
self.schedule_update_timeout_handle = None;
|
||||
self.schedule_update_fn = None;
|
||||
}
|
||||
|
||||
fn reset_debounce_timeout_and_schedule_update<FOut: std::fmt::Debug + 'static>(
|
||||
&mut self,
|
||||
ctx: &mut ViewCtx,
|
||||
debounce_duration: usize,
|
||||
) {
|
||||
ctx.with_id(ViewId::new(self.generation), |ctx| {
|
||||
self.clear_update_timeout();
|
||||
let thunk = ctx.message_thunk();
|
||||
let schedule_update_fn = Closure::new(move || {
|
||||
thunk.push_message(MemoizedAwaitMessage::<FOut>::ScheduleUpdate);
|
||||
});
|
||||
let handle = web_sys::window()
|
||||
.unwrap_throw()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
schedule_update_fn.as_ref().unchecked_ref(),
|
||||
debounce_duration.try_into().unwrap_throw(),
|
||||
)
|
||||
.unwrap_throw();
|
||||
self.schedule_update_fn = Some(schedule_update_fn);
|
||||
self.schedule_update_timeout_handle = Some(handle);
|
||||
self.schedule_update = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MemoizedAwaitMessage<Output: std::fmt::Debug> {
|
||||
Output(Output),
|
||||
ScheduleUpdate,
|
||||
}
|
||||
|
||||
impl<State, Action, InitFuture, F, FOut, Data, CB, OA> View<State, Action, ViewCtx, DynMessage>
|
||||
for MemoizedAwait<State, Action, OA, InitFuture, Data, CB, F, FOut>
|
||||
where
|
||||
State: 'static,
|
||||
Action: 'static,
|
||||
OA: OptionalAction<Action> + 'static,
|
||||
InitFuture: Fn(&Data) -> F + 'static,
|
||||
FOut: std::fmt::Debug + 'static,
|
||||
Data: PartialEq + 'static,
|
||||
F: Future<Output = FOut> + 'static,
|
||||
CB: Fn(&mut State, FOut) -> OA + 'static,
|
||||
{
|
||||
type Element = NoElement;
|
||||
|
||||
type ViewState = MemoizedAwaitState;
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
let mut state = MemoizedAwaitState::default();
|
||||
|
||||
if self.debounce_ms > 0 {
|
||||
state.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
|
||||
} else {
|
||||
self.init_future(ctx, state.generation);
|
||||
}
|
||||
|
||||
(NoElement, state)
|
||||
}
|
||||
|
||||
fn rebuild<'el>(
|
||||
&self,
|
||||
prev: &Self,
|
||||
view_state: &mut Self::ViewState,
|
||||
ctx: &mut ViewCtx,
|
||||
(): Mut<'el, Self::Element>,
|
||||
) -> Mut<'el, Self::Element> {
|
||||
let debounce_has_changed_and_update_is_scheduled = view_state.schedule_update
|
||||
&& (prev.reset_debounce_on_update != self.reset_debounce_on_update
|
||||
|| prev.debounce_ms != self.debounce_ms);
|
||||
|
||||
if debounce_has_changed_and_update_is_scheduled {
|
||||
if self.debounce_ms == 0 {
|
||||
if view_state.schedule_update_timeout_handle.is_some() {
|
||||
view_state.clear_update_timeout();
|
||||
view_state.schedule_update = false;
|
||||
view_state.update = true;
|
||||
}
|
||||
} else {
|
||||
view_state
|
||||
.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
|
||||
return; // avoid update below, as it's already scheduled
|
||||
}
|
||||
}
|
||||
|
||||
if view_state.update
|
||||
|| (prev.data != self.data
|
||||
&& (!view_state.schedule_update || self.reset_debounce_on_update))
|
||||
{
|
||||
if !view_state.update && self.debounce_ms > 0 {
|
||||
view_state
|
||||
.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
|
||||
} else {
|
||||
// no debounce
|
||||
view_state.generation += 1;
|
||||
view_state.update = false;
|
||||
self.init_future(ctx, view_state.generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn teardown(&self, state: &mut Self::ViewState, _: &mut ViewCtx, (): Mut<'_, Self::Element>) {
|
||||
state.clear_update_timeout();
|
||||
}
|
||||
|
||||
fn message(
|
||||
&self,
|
||||
view_state: &mut Self::ViewState,
|
||||
id_path: &[ViewId],
|
||||
message: DynMessage,
|
||||
app_state: &mut State,
|
||||
) -> MessageResult<Action, DynMessage> {
|
||||
assert_eq!(id_path.len(), 1);
|
||||
if id_path[0].routing_id() == view_state.generation {
|
||||
match *message.downcast().unwrap_throw() {
|
||||
MemoizedAwaitMessage::Output(future_output) => {
|
||||
match (self.callback)(app_state, future_output).action() {
|
||||
Some(action) => MessageResult::Action(action),
|
||||
None => MessageResult::Nop,
|
||||
}
|
||||
}
|
||||
MemoizedAwaitMessage::ScheduleUpdate => {
|
||||
view_state.update = true;
|
||||
view_state.schedule_update = false;
|
||||
MessageResult::RequestRebuild
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessageResult::Stale(message)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2024 the Xilem Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Async views, allowing concurrent operations, like fetching data from a server
|
||||
|
||||
mod memoized_await;
|
||||
|
||||
pub use memoized_await::{memoized_await, MemoizedAwait};
|
|
@ -43,6 +43,7 @@ mod text;
|
|||
mod vec_splice;
|
||||
mod vecmap;
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod elements;
|
||||
pub mod interfaces;
|
||||
pub mod svg;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "fetch"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
gloo-net = { version = "0.5.0", default-features = false, features = ["http", "json", "serde"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
web-sys = { version = "0.3.69", features = ["Event", "HtmlInputElement"] }
|
||||
wasm-bindgen = "0.2.92"
|
||||
xilem_web = { path = "../.." }
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
</head>
|
||||
<style>
|
||||
img {
|
||||
max-width: 250px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid red;
|
||||
color: red;
|
||||
background-color: lightpink;
|
||||
}
|
||||
|
||||
.cat-fetch-controls {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blinker 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
from {
|
||||
color: blueviolet;
|
||||
}
|
||||
|
||||
50% {
|
||||
color: hotpink;
|
||||
}
|
||||
|
||||
to {
|
||||
color: blueviolet;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<body></body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2024 the Xilem Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use xilem_web::{
|
||||
concurrent::memoized_await,
|
||||
core::{fork, one_of::Either},
|
||||
document_body,
|
||||
elements::html::*,
|
||||
interfaces::{Element, HtmlDivElement, HtmlImageElement, HtmlLabelElement},
|
||||
App,
|
||||
};
|
||||
|
||||
use gloo_net::http::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TOO_MANY_CATS: usize = 8;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Cat {
|
||||
pub url: String,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
cats_to_fetch: usize,
|
||||
cats_are_being_fetched: bool,
|
||||
cats: Vec<Cat>,
|
||||
debounce_in_ms: usize,
|
||||
reset_debounce_on_update: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cats_to_fetch: 0,
|
||||
cats: Vec::new(),
|
||||
debounce_in_ms: 1000,
|
||||
cats_are_being_fetched: false,
|
||||
reset_debounce_on_update: true,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: usize) -> Result<Vec<Cat>, gloo_net::Error> {
|
||||
log::debug!("Fetch {count} cats");
|
||||
if count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let url = format!("https://api.thecatapi.com/v1/images/search?limit={count}");
|
||||
Ok(Request::get(&url)
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<Cat>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(count)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn input_target<T>(event: &T) -> web_sys::HtmlInputElement
|
||||
where
|
||||
T: JsCast,
|
||||
{
|
||||
event
|
||||
.unchecked_ref::<web_sys::Event>()
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
}
|
||||
|
||||
fn app_logic(state: &mut AppState) -> impl HtmlDivElement<AppState> {
|
||||
div((
|
||||
cat_fetch_controls(state),
|
||||
fork(
|
||||
cat_images_and_fetching_indicator(state),
|
||||
// Here's the actual fetching logic:
|
||||
(state.cats_to_fetch < TOO_MANY_CATS).then_some(
|
||||
memoized_await(
|
||||
// This is given to the first closure right below which when resolved invokes the second closure with the output of the future,
|
||||
// and when it changes, that first closure will be reevaluated again (similarly as the `Memoize` view).
|
||||
// If `debounce_ms` below > `0`, then further updates (i.e. invocation of `fetch_cats`) are either throttled (when `!reset_debounce_on_update`),
|
||||
// or debounced otherwise:
|
||||
// As long as updates are happening within `debounce_in_ms` ms the first closure is not invoked, and a debounce timeout which runs `debounce_in_ms` is reset.
|
||||
state.cats_to_fetch,
|
||||
|count| fetch_cats(*count),
|
||||
|state: &mut AppState, cats_result| match cats_result {
|
||||
Ok(cats) => {
|
||||
log::info!("Received {} cats", cats.len());
|
||||
state.cats = cats;
|
||||
state.cats_are_being_fetched = false;
|
||||
state.error = None;
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Unable to fetch cats: {err:#}");
|
||||
state.cats_are_being_fetched = false;
|
||||
state.error = Some(err.to_string());
|
||||
}
|
||||
},
|
||||
)
|
||||
.debounce_ms(state.debounce_in_ms)
|
||||
.reset_debounce_on_update(state.reset_debounce_on_update),
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn cat_images_and_fetching_indicator(state: &AppState) -> impl HtmlDivElement<AppState> {
|
||||
let cat_images = state
|
||||
.cats
|
||||
.iter()
|
||||
.map(|cat| {
|
||||
img(())
|
||||
.src(cat.url.clone())
|
||||
.attr("width", cat.width)
|
||||
.attr("height", cat.height)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let error_message = state
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| div((h2("Error"), p(err.to_string()))).class("error"));
|
||||
div((
|
||||
error_message,
|
||||
if state.cats_to_fetch != 0 && state.cats_to_fetch == cat_images.len() {
|
||||
Either::A(h1("Here are your cats:").class("blink"))
|
||||
} else if state.cats_to_fetch >= TOO_MANY_CATS {
|
||||
Either::B(p("Woah there, that's too many cats"))
|
||||
} else if state.debounce_in_ms > 0
|
||||
&& state.cats_to_fetch > 0
|
||||
&& state.reset_debounce_on_update
|
||||
{
|
||||
Either::B(p("Debounced fetch of cats..."))
|
||||
} else if state.debounce_in_ms > 0 && state.cats_to_fetch > 0 {
|
||||
Either::B(p("Throttled fetch of cats..."))
|
||||
} else if state.cats_to_fetch > 0 && state.cats_are_being_fetched {
|
||||
Either::B(p("Fetching cats..."))
|
||||
} else {
|
||||
Either::B(p("You need to fetch cats"))
|
||||
},
|
||||
cat_images,
|
||||
))
|
||||
}
|
||||
|
||||
fn cat_fetch_controls(state: &AppState) -> impl Element<AppState> {
|
||||
fieldset((
|
||||
legend("Cat fetch controls"),
|
||||
table((
|
||||
tr((
|
||||
td(label("How many cats would you like?").for_("cat-count")),
|
||||
td(input(())
|
||||
.id("cat-count")
|
||||
.attr("type", "number")
|
||||
.attr("min", 0)
|
||||
.attr("value", state.cats_to_fetch)
|
||||
.on_input(|state: &mut AppState, ev: web_sys::Event| {
|
||||
if !state.cats_are_being_fetched {
|
||||
state.cats.clear();
|
||||
}
|
||||
state.cats_are_being_fetched = true;
|
||||
state.cats_to_fetch = input_target(&ev).value().parse().unwrap_or(0);
|
||||
})),
|
||||
)),
|
||||
tr((
|
||||
td(
|
||||
label("Reset fetch debounce timeout when updating the cat count:")
|
||||
.for_("reset-debounce-update"),
|
||||
),
|
||||
td(input(())
|
||||
.id("reset-debounce-update")
|
||||
.attr("type", "checkbox")
|
||||
.attr("checked", state.reset_debounce_on_update)
|
||||
.on_input(|state: &mut AppState, event: web_sys::Event| {
|
||||
state.reset_debounce_on_update = input_target(&event).checked();
|
||||
})),
|
||||
)),
|
||||
tr((
|
||||
td(label("Debounce timeout in ms:").for_("debounce-timeout-duration")),
|
||||
td(input(())
|
||||
.id("debounce-timeout-duration")
|
||||
.attr("type", "number")
|
||||
.attr("min", 0)
|
||||
.attr("value", state.debounce_in_ms)
|
||||
.on_input(|state: &mut AppState, ev: web_sys::Event| {
|
||||
state.debounce_in_ms = input_target(&ev).value().parse().unwrap_or(0);
|
||||
})),
|
||||
)),
|
||||
)),
|
||||
))
|
||||
.class("cat-fetch-controls")
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
Loading…
Reference in New Issue