yew_bs/components/
toasts.rs

1use yew::prelude::*;
2use web_sys::Element;
3use wasm_bindgen::JsValue;
4use crate::components::common::Variant;
5use crate::interop::BsToast;
6#[derive(Clone, Copy, PartialEq, Debug)]
7pub enum ToastPlacement {
8    TopStart,
9    TopCenter,
10    TopEnd,
11    MiddleStart,
12    MiddleCenter,
13    MiddleEnd,
14    BottomStart,
15    BottomCenter,
16    BottomEnd,
17}
18impl ToastPlacement {
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            ToastPlacement::TopStart => "top-0 start-0",
22            ToastPlacement::TopCenter => "top-0 start-50 translate-middle-x",
23            ToastPlacement::TopEnd => "top-0 end-0",
24            ToastPlacement::MiddleStart => "top-50 start-0 translate-middle-y",
25            ToastPlacement::MiddleCenter => "top-50 start-50 translate-middle",
26            ToastPlacement::MiddleEnd => "top-50 end-0 translate-middle-y",
27            ToastPlacement::BottomStart => "bottom-0 start-0",
28            ToastPlacement::BottomCenter => "bottom-0 start-50 translate-middle-x",
29            ToastPlacement::BottomEnd => "bottom-0 end-0",
30        }
31    }
32}
33/// Props for the Toast component
34#[derive(Properties, PartialEq)]
35pub struct ToastProps {
36    /// Toast header content
37    #[prop_or_default]
38    pub header: Children,
39    /// Toast body content
40    #[prop_or_default]
41    pub children: Children,
42    /// Whether the toast is currently shown
43    #[prop_or(true)]
44    pub show: bool,
45    /// Whether the toast can be dismissed
46    #[prop_or(true)]
47    pub dismissible: bool,
48    /// Toast variant (color)
49    #[prop_or_default]
50    pub variant: Option<Variant>,
51    /// Autohide delay in milliseconds (0 = no autohide)
52    #[prop_or(5000)]
53    pub delay: u32,
54    /// Callback when toast is shown
55    #[prop_or_default]
56    pub on_show: Option<Callback<()>>,
57    /// Callback when toast is hidden
58    #[prop_or_default]
59    pub on_hide: Option<Callback<()>>,
60    /// Additional CSS classes
61    #[prop_or_default]
62    pub class: Option<AttrValue>,
63    /// Additional HTML attributes
64    #[prop_or_default]
65    pub node_ref: NodeRef,
66}
67/// Props for the ToastContainer component
68#[derive(Properties, PartialEq)]
69pub struct ToastContainerProps {
70    /// Toast container placement
71    #[prop_or(ToastPlacement::TopEnd)]
72    pub placement: ToastPlacement,
73    /// Container children (usually Toast components)
74    #[prop_or_default]
75    pub children: Children,
76    /// Additional CSS classes
77    #[prop_or_default]
78    pub class: Option<AttrValue>,
79    /// Additional HTML attributes
80    #[prop_or_default]
81    pub node_ref: NodeRef,
82}
83/// Bootstrap Toast component
84#[function_component(Toast)]
85pub fn toast(props: &ToastProps) -> Html {
86    let mut classes = Classes::new();
87    classes.push("toast");
88    if let Some(variant) = &props.variant {
89        classes.push(format!("text-bg-{}", variant.as_str()));
90    }
91    if let Some(class) = &props.class {
92        classes.push(class.to_string());
93    }
94    let on_show = props.on_show.clone();
95    let on_hide = props.on_hide.clone();
96    let node_ref = props.node_ref.clone();
97    let dismissible = props.dismissible;
98    let options = {
99        let opts = js_sys::Object::new();
100        js_sys::Reflect::set(&opts, &"autohide".into(), &JsValue::from(props.delay > 0))
101            .unwrap();
102        if props.delay > 0 {
103            js_sys::Reflect::set(&opts, &"delay".into(), &JsValue::from(props.delay))
104                .unwrap();
105        }
106        JsValue::from(opts)
107    };
108    {
109        let node_ref = node_ref.clone();
110        let on_show = on_show.clone();
111        let on_hide = on_hide.clone();
112        use_effect_with(
113            (props.show, options.clone()),
114            move |(show, options)| {
115                if let Some(element) = node_ref.cast::<Element>() {
116                    let bs_toast = BsToast::new(&element, Some(&options));
117                    if *show {
118                        bs_toast.show();
119                        if let Some(on_show) = &on_show {
120                            on_show.emit(());
121                        }
122                    } else {
123                        bs_toast.hide();
124                        if let Some(on_hide) = &on_hide {
125                            on_hide.emit(());
126                        }
127                    }
128                }
129                || ()
130            },
131        );
132    }
133    let close_button_onclick = Callback::from(move |e: MouseEvent| {
134        e.prevent_default();
135        if let Some(element) = node_ref.cast::<Element>() {
136            let bs_toast = BsToast::new(&element, Some(&options));
137            bs_toast.hide();
138            if let Some(on_hide) = on_hide.as_ref() {
139                on_hide.emit(());
140            }
141        }
142    });
143    html! {
144        < div class = { classes } role = "alert" aria - live = "assertive" aria - atomic
145        = "true" ref = { props.node_ref.clone() } > if ! props.header.is_empty() { < div
146        class = "toast-header" > { for props.header.iter() } if dismissible { < button
147        type = "button" class = "btn-close" data - bs - dismiss = "toast" aria - label =
148        "Close" onclick = { close_button_onclick.clone() } /> } </ div > } < div class =
149        "toast-body" > { for props.children.iter() } if props.header.is_empty() &&
150        dismissible { < button type = "button" class = "btn-close ms-auto" data - bs -
151        dismiss = "toast" aria - label = "Close" onclick = { close_button_onclick } /> }
152        </ div > </ div >
153    }
154}
155/// Bootstrap Toast Container component
156#[function_component(ToastContainer)]
157pub fn toast_container(props: &ToastContainerProps) -> Html {
158    let mut classes = Classes::new();
159    classes.push("toast-container");
160    classes.push("position-fixed");
161    classes.push("p-3");
162    for class in props.placement.as_str().split(' ') {
163        classes.push(class);
164    }
165    if let Some(class) = &props.class {
166        classes.push(class.to_string());
167    }
168    html! {
169        < div class = { classes } ref = { props.node_ref.clone() } > { for props.children
170        .iter() } </ div >
171    }
172}