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"
# leptos-use = "0.10.10"
send_wrapper = "0.6"
uuid = { version = "1.10.0", features = ["v4", "js"] }
console_error_panic_hook = "0.1.7"
console_log = "1"
log = "0.4"

View file

@ -1,5 +1,6 @@
use crate::components::{Demo, DemoCode};
use leptos::{ev, prelude::*};
use thaw::*;
use uuid;
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
```rust demo

View file

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

View file

@ -63,6 +63,12 @@ impl Default for 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.
pub fn with_position(mut self, position: ToastPosition) -> Self {
self.position = Some(position);

View file

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