Toast dismiss (#270)

* broken toaster

* toast dismiss

* toaster test

* fixed dismiss

* cargo fmt

* dipose signal
This commit is contained in:
kandrelczyk 2024-09-29 05:22:14 +02:00 committed by GitHub
parent a03226f202
commit b831008e09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 159 additions and 41 deletions

View file

@ -18,6 +18,7 @@ chrono = "0.4.38"
cfg-if = "1.0.0" cfg-if = "1.0.0"
# leptos-use = "0.10.10" # leptos-use = "0.10.10"
send_wrapper = "0.6" send_wrapper = "0.6"
uuid = { version = "1.10.0", features = ["v4", "js"] }
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
console_log = "1" console_log = "1"
log = "0.4" log = "0.4"

View file

@ -1,5 +1,6 @@
use crate::components::{Demo, DemoCode}; use crate::components::{Demo, DemoCode};
use leptos::{ev, prelude::*}; use leptos::{ev, prelude::*};
use thaw::*; use thaw::*;
use uuid;
demo_markdown::include_md! {} demo_markdown::include_md! {}

View file

@ -95,6 +95,38 @@ view! {
} }
``` ```
### Dismiss Toast
```rust demo
let toaster = ToasterInjection::expect_context();
let id = uuid::Uuid::new_v4();
let dispatch = move |_| {
toaster.dispatch_toast(move || view! {
<Toast>
<ToastTitle>"Email sent"</ToastTitle>
<ToastBody>
"This is a toast body"
<ToastBodySubtitle slot>
"Subtitle"
</ToastBodySubtitle>
</ToastBody>
</Toast>
},ToastOptions::default().with_id(id));
};
let dismiss = move |_| {
toaster.dismiss_toast(id);
};
view! {
<Button on_click=dispatch>"Show toast"</Button>
<Button on_click=dismiss>"Hide toast"</Button>
}
```
### Toast Title Media ### Toast Title Media
```rust demo ```rust demo

View file

@ -17,17 +17,22 @@ use wasm_bindgen::UnwrapThrowExt;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ToasterInjection { pub struct ToasterInjection {
sender: StoredValue<Sender<(Children, ToastOptions)>>, sender: StoredValue<Sender<ToasterMessage>>,
trigger: StoredValue<ArcTrigger>, trigger: StoredValue<ArcTrigger>,
} }
enum ToasterMessage {
Dispatch(Children, ToastOptions),
Dismiss(uuid::Uuid),
}
impl ToasterInjection { impl ToasterInjection {
pub fn expect_context() -> Self { pub fn expect_context() -> Self {
expect_context() expect_context()
} }
pub fn channel() -> (Self, ToasterReceiver) { pub fn channel() -> (Self, ToasterReceiver) {
let (sender, receiver) = channel::<(Children, ToastOptions)>(); let (sender, receiver) = channel::<ToasterMessage>();
let trigger = ArcTrigger::new(); let trigger = ArcTrigger::new();
( (
@ -39,6 +44,15 @@ impl ToasterInjection {
) )
} }
pub fn dismiss_toast(&self, toast_id: uuid::Uuid) {
self.sender.with_value(|sender| {
sender
.send(ToasterMessage::Dismiss(toast_id))
.unwrap_throw()
});
self.trigger.with_value(|trigger| trigger.notify());
}
pub fn dispatch_toast<C, IV>(&self, children: C, options: ToastOptions) pub fn dispatch_toast<C, IV>(&self, children: C, options: ToastOptions)
where where
C: FnOnce() -> IV + Send + 'static, C: FnOnce() -> IV + Send + 'static,
@ -46,7 +60,10 @@ impl ToasterInjection {
{ {
self.sender.with_value(|sender| { self.sender.with_value(|sender| {
sender sender
.send((Box::new(move || children().into_any()), options)) .send(ToasterMessage::Dispatch(
Box::new(move || children().into_any()),
options,
))
.unwrap_throw() .unwrap_throw()
}); });
self.trigger.with_value(|trigger| trigger.notify()); self.trigger.with_value(|trigger| trigger.notify());
@ -54,16 +71,16 @@ impl ToasterInjection {
} }
pub struct ToasterReceiver { pub struct ToasterReceiver {
receiver: Receiver<(Children, ToastOptions)>, receiver: Receiver<ToasterMessage>,
trigger: ArcTrigger, trigger: ArcTrigger,
} }
impl ToasterReceiver { impl ToasterReceiver {
pub fn new(receiver: Receiver<(Children, ToastOptions)>, trigger: ArcTrigger) -> Self { fn new(receiver: Receiver<ToasterMessage>, trigger: ArcTrigger) -> Self {
Self { receiver, trigger } Self { receiver, trigger }
} }
pub fn try_recv(&self) -> TryIter<'_, (Children, ToastOptions)> { fn try_recv(&self) -> TryIter<'_, ToasterMessage> {
self.trigger.track(); self.trigger.track();
self.receiver.try_iter() self.receiver.try_iter()
} }

View file

@ -63,6 +63,12 @@ impl Default for ToastOptions {
} }
impl ToastOptions { impl ToastOptions {
/// The id that will be assigned to this toast.
pub fn with_id(mut self, id: uuid::Uuid) -> Self {
self.id = id;
self
}
/// The position the toast should render. /// The position the toast should render.
pub fn with_position(mut self, position: ToastPosition) -> Self { pub fn with_position(mut self, position: ToastPosition) -> Self {
self.position = Some(position); self.position = Some(position);

View file

@ -1,5 +1,5 @@
use super::{ToastIntent, ToastOptions, ToastPosition, ToasterReceiver}; use super::{ToastIntent, ToastOptions, ToastPosition, ToasterReceiver};
use crate::ConfigInjection; use crate::{toast::ToasterMessage, ConfigInjection};
use leptos::{context::Provider, either::Either, html, prelude::*}; use leptos::{context::Provider, either::Either, html, prelude::*};
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
@ -22,9 +22,11 @@ pub fn Toaster(
let bottom_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default()); let bottom_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default()); let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let bottom_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default()); let bottom_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
let toasts = StoredValue::<HashMap<uuid::Uuid, (SendWrapper<Children>, ToastOptions)>>::new( let toasts = StoredValue::<
Default::default(), HashMap<uuid::Uuid, (SendWrapper<Children>, ToastOptions, RwSignal<bool>)>,
); >::new(Default::default());
let toast_show_list =
StoredValue::<HashMap<uuid::Uuid, RwSignal<bool>>>::new(Default::default());
let id_list = move |position: &ToastPosition| match position { let id_list = move |position: &ToastPosition| match position {
ToastPosition::Top => top_id_list, ToastPosition::Top => top_id_list,
@ -35,26 +37,38 @@ pub fn Toaster(
ToastPosition::BottomEnd => bottom_end_id_list, ToastPosition::BottomEnd => bottom_end_id_list,
}; };
let owner = Owner::current().unwrap();
Effect::new(move |_| { Effect::new(move |_| {
for (view, mut options) in receiver.try_recv() { for message in receiver.try_recv() {
if options.position.is_none() { match message {
options.position = Some(position); ToasterMessage::Dispatch(view, mut options) => {
} if options.position.is_none() {
if options.timeout.is_none() { options.position = Some(position);
options.timeout = Some(timeout); }
} if options.timeout.is_none() {
if options.intent.is_none() { options.timeout = Some(timeout);
options.intent = Some(intent); }
} if options.intent.is_none() {
options.intent = Some(intent);
}
let list = id_list(&options.position.unwrap_throw()); let list = id_list(&options.position.unwrap_throw());
let id = options.id; let id = options.id;
toasts.update_value(|map| { let is_show = owner.with(|| RwSignal::new(true));
map.insert(id, (SendWrapper::new(view), options)); toasts.update_value(|map| {
}); map.insert(id, (SendWrapper::new(view), options, is_show));
list.update(|list| { });
list.push(id); toast_show_list.update_value(|map| {
}); map.insert(id, is_show);
});
list.update(|list| {
list.push(id);
});
}
ToasterMessage::Dismiss(toast_id) => {
toast_show_list.with_value(|map| map.get(&toast_id).unwrap_throw().set(false));
}
}
} }
}); });
@ -66,6 +80,10 @@ pub fn Toaster(
}; };
list.remove(index); list.remove(index);
}); });
let is_show = toast_show_list.try_update_value(|map| { map.remove(&id) } ).flatten();
if let Some(is_show) = is_show {
is_show.dispose();
}
})); }));
view! { view! {
@ -76,12 +94,19 @@ pub fn Toaster(
> >
<div class="thaw-toaster thaw-toaster--top"> <div class="thaw-toaster thaw-toaster--top">
<For each=move || top_id_list.get() key=|id| id.clone() let:id> <For each=move || top_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -90,12 +115,19 @@ pub fn Toaster(
</div> </div>
<div class="thaw-toaster thaw-toaster--top-start"> <div class="thaw-toaster thaw-toaster--top-start">
<For each=move || top_start_id_list.get() key=|id| id.clone() let:id> <For each=move || top_start_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -104,12 +136,19 @@ pub fn Toaster(
</div> </div>
<div class="thaw-toaster thaw-toaster--top-end"> <div class="thaw-toaster thaw-toaster--top-end">
<For each=move || top_end_id_list.get() key=|id| id.clone() let:id> <For each=move || top_end_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -118,12 +157,19 @@ pub fn Toaster(
</div> </div>
<div class="thaw-toaster thaw-toaster--bottom"> <div class="thaw-toaster thaw-toaster--bottom">
<For each=move || bottom_id_list.get() key=|id| id.clone() let:id> <For each=move || bottom_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -132,12 +178,19 @@ pub fn Toaster(
</div> </div>
<div class="thaw-toaster thaw-toaster--bottom-start"> <div class="thaw-toaster thaw-toaster--bottom-start">
<For each=move || bottom_start_id_list.get() key=|id| id.clone() let:id> <For each=move || bottom_start_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -146,12 +199,19 @@ pub fn Toaster(
</div> </div>
<div class="thaw-toaster thaw-toaster--bottom-end"> <div class="thaw-toaster thaw-toaster--bottom-end">
<For each=move || bottom_end_id_list.get() key=|id| id.clone() let:id> <For each=move || bottom_end_id_list.get() key=|id| id.clone() let:id>
{if let Some((view, options)) = toasts {if let Some((view, options, is_show)) = toasts
.try_update_value(|map| { map.remove(&id) }) .try_update_value(|map| { map.remove(&id) })
.flatten() .flatten()
{ {
Either::Left( Either::Left(
view! { <ToasterContainer on_close children=view.take() options /> }, view! {
<ToasterContainer
on_close
children=view.take()
options
is_show
/>
},
) )
} else { } else {
Either::Right(()) Either::Right(())
@ -168,9 +228,9 @@ fn ToasterContainer(
options: ToastOptions, options: ToastOptions,
#[prop(into)] on_close: StoredValue<ArcTwoCallback<uuid::Uuid, ToastPosition>>, #[prop(into)] on_close: StoredValue<ArcTwoCallback<uuid::Uuid, ToastPosition>>,
children: Children, children: Children,
is_show: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let container_ref = NodeRef::<html::Div>::new(); let container_ref = NodeRef::<html::Div>::new();
let is_show = RwSignal::new(true);
let ToastOptions { let ToastOptions {
id, id,
timeout, timeout,
@ -178,6 +238,7 @@ fn ToasterContainer(
intent, intent,
.. ..
} = options; } = options;
let timeout = timeout.unwrap_throw(); let timeout = timeout.unwrap_throw();
let position = position.unwrap_throw(); let position = position.unwrap_throw();
let intent = intent.unwrap_throw(); let intent = intent.unwrap_throw();