yew_alert/
lib.rs

1#![doc(
2    html_logo_url = "https://github.com/next-rs/yew-alert/assets/62179149/b4b97406-f4a5-4235-a255-b0ffec31a05b",
3    html_favicon_url = "https://github.com/next-rs/yew-alert/assets/62179149/03114e06-dcf8-4121-91c7-5ddc43300a43"
4)]
5
6//! # Yew Alert - Documentation
7//!
8//! Welcome to the official Yew Alert library documentation. This library
9//! provides a flexible and customizable alert component for your Yew applications.
10//!
11//! ## Usage
12//!
13//! To use the Yew Alert library, add the following dependency to your `Cargo.toml` file:
14//!
15//! ```sh
16//! cargo add yew-alert
17//! ```
18//!
19//! To integrate the library into your Yew application, you can use the `Alert` component.
20//! Here's a simple example of how to use it:
21//!
22//! ```rust,no_run
23//! use yew::prelude::*;
24//! use yew_alert::Alert;
25//!
26//! // Your Yew component structure here...
27//!
28//! #[function_component]
29//! pub fn MyComponent() -> Html {
30//!     // Your component logic here...
31//!     let show_alert = use_state(|| true);
32//!
33//!     html! {
34//!         <div>
35//!             <button onclick=/* Your callback function here to trigger the alert */>{"Show Alert"}</button>
36//!             <Alert
37//!                 message="This is a sample alert message."
38//!                 show_alert={show_alert} // Use your state or logic to control visibility
39//!                 on_confirm=Callback::from(/* Your confirm callback function here */)
40//!                 on_cancel=Callback::from(/* Your cancel callback function here */)
41//!             />
42//!         </div>
43//!     }
44//! }
45//! ```
46//!
47//! For more detailed information, check the [examples] provided in the library.
48//!
49//! [examples]: https://github.com/next-rs/yew-alert/examples/tailwind
50//!
51//! ## Configuration
52//!
53//! Yew Alert allows you to customize various aspects of the alert component through the
54//! `AlertProps` structure. You can adjust properties such as message content, button text,
55//! styling, and more. Refer to the `AlertProps` documentation for detailed configuration options.
56//!
57//! ```rust,no_run
58//! use yew_alert::{AlertProps, Alert};
59//!
60//! let alert_props = AlertProps {
61//!     message: "Custom alert message",
62//!     // Add other properties as needed...
63//!     // ...
64//! };
65//!
66//! let alert_component = html! {
67//!     <Alert ..alert_props />
68//! };
69//! ```
70//!
71//! ## Contribution and Support
72//!
73//! If you encounter any issues or have suggestions for improvements, feel free to contribute
74//! to the [GitHub repository](https://github.com/next-rs/yew-alert). We appreciate your feedback
75//! and involvement in making Yew Alert better!
76//!
77//! ## Acknowledgments
78//!
79//! Special thanks to the Yew community and contributors for such an amazing framework.
80
81use gloo::timers::callback::Timeout;
82use yew::prelude::*;
83
84const TITLE: &'static str = "Info";
85const ALERT_CLASS: &'static str = "w-96 h-48 text-white";
86const ICON_CLASS: &'static str = "flex justify-center";
87const CONFIRM_BUTTON_TEXT: &'static str = "Okay";
88const CANCEL_BUTTON_TEXT: &'static str = "Cancel";
89const CONFIRM_BUTTON_CLASS: &'static str =
90    "mt-2 mx-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 focus:outline-none focus:border-blue-700 focus:ring focus:ring-blue-200";
91const CANCEL_BUTTON_CLASS: &'static str =
92    "mt-2 mx-2 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 focus:outline-none focus:border-gray-700 focus:ring focus:ring-gray-200";
93const POSITION: &'static str = "top-right";
94const CONTAINER_CLASS: &'static str =
95    "flex items-center text-center justify-center bg-gray-800 text-white border border-gray-600";
96const TITLE_CLASS: &'static str = "dark:text-white";
97const MESSAGE_CLASS: &'static str = "dark:text-gray-300";
98const ICON_TYPE: &'static str = "success";
99const ICON_COLOR: &'static str = "";
100const ICON_WIDTH: &'static str = "50";
101
102/// Props for the Alert component.
103#[derive(Debug, PartialEq, Properties, Clone)]
104pub struct AlertProps {
105    /// The message to be displayed in the alert.
106    #[prop_or_default]
107    pub message: &'static str,
108
109    /// State handle to control the visibility of the alert.
110    pub show_alert: UseStateHandle<bool>,
111
112    /// Time duration in milliseconds before the alert automatically closes.
113    #[prop_or(2500)]
114    pub timeout: u32,
115
116    /// The title of the alert.
117    #[prop_or(TITLE)]
118    pub title: &'static str,
119
120    /// CSS class for styling the alert.
121    #[prop_or(ALERT_CLASS)]
122    pub alert_class: &'static str,
123
124    /// CSS class for styling the icon in the alert.
125    #[prop_or(ICON_CLASS)]
126    pub icon_class: &'static str,
127
128    /// Text for the confirm button in the alert.
129    #[prop_or(CONFIRM_BUTTON_TEXT)]
130    pub confirm_button_text: &'static str,
131
132    /// Text for the cancel button in the alert.
133    #[prop_or(CANCEL_BUTTON_TEXT)]
134    pub cancel_button_text: &'static str,
135
136    /// CSS class for styling the confirm button.
137    #[prop_or(CONFIRM_BUTTON_CLASS)]
138    pub confirm_button_class: &'static str,
139
140    /// CSS class for styling the cancel button.
141    #[prop_or(CANCEL_BUTTON_CLASS)]
142    pub cancel_button_class: &'static str,
143
144    /// Flag to determine if the confirm button should be displayed.
145    #[prop_or(true)]
146    pub show_confirm_button: bool,
147
148    /// Flag to determine if the cancel button should be displayed.
149    #[prop_or(true)]
150    pub show_cancel_button: bool,
151
152    /// Flag to determine if the close button should be displayed.
153    #[prop_or(false)]
154    pub show_close_button: bool,
155
156    /// Callback function triggered on confirm button click.
157    #[prop_or_default]
158    pub on_confirm: Callback<()>,
159
160    /// Callback function triggered on cancel button click.
161    #[prop_or_default]
162    pub on_cancel: Callback<()>,
163
164    /// Position of the alert on the screen (e.g., "top-left", "middle", "bottom-right").
165    #[prop_or(POSITION)]
166    pub position: &'static str,
167
168    /// CSS class for styling the alert container.
169    #[prop_or(CONTAINER_CLASS)]
170    pub container_class: &'static str,
171
172    /// CSS class for styling the title in the alert.
173    #[prop_or(TITLE_CLASS)]
174    pub title_class: &'static str,
175
176    /// CSS class for styling the message in the alert.
177    #[prop_or(MESSAGE_CLASS)]
178    pub message_class: &'static str,
179
180    /// Type of the icon to be displayed in the alert (e.g., "warning", "error", "success").
181    #[prop_or(ICON_TYPE)]
182    pub icon_type: &'static str,
183
184    /// Color of the icon in the alert.
185    #[prop_or(ICON_COLOR)]
186    pub icon_color: &'static str,
187
188    /// Width of the icon in the alert.
189    #[prop_or(ICON_WIDTH)]
190    pub icon_width: &'static str,
191}
192
193/// Alert Component
194#[function_component]
195pub fn Alert(props: &AlertProps) -> Html {
196    let show = *props.show_alert;
197    let timeout = props.timeout.clone();
198    let show_alert = props.show_alert.clone();
199
200    use_effect_with(show_alert.clone(), move |show_alert| {
201        if **show_alert {
202            let show_alert = show_alert.clone();
203            let handle = Timeout::new(timeout, move || show_alert.set(false)).forget();
204            let clear_handle = move || {
205                web_sys::Window::clear_timeout_with_handle(
206                    &web_sys::window().unwrap(),
207                    handle.as_f64().unwrap() as i32,
208                );
209            };
210
211            Box::new(clear_handle) as Box<dyn FnOnce()>
212        } else {
213            Box::new(|| {}) as Box<dyn FnOnce()>
214        }
215    });
216
217    let on_cancel = {
218        let on_cancel = props.on_cancel.clone();
219
220        Callback::from(move |_| {
221            on_cancel.emit(());
222            show_alert.set(false);
223        })
224    };
225    let on_confirm = {
226        let on_confirm = props.on_confirm.clone();
227
228        Callback::from(move |_| {
229            on_confirm.emit(());
230        })
231    };
232
233    let position_class = match props.position {
234        "top-left" => "top-0 left-0",
235        "top-right" => "top-0 right-0",
236        "middle" => "top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2",
237        "bottom" => "bottom-0 left-1/2 transform -translate-x-1/2",
238        "top" => "top-0 left-1/2 transform -translate-x-1/2",
239        "bottom-right" => "bottom-0 right-0",
240        "bottom-left" => "bottom-0 left-0",
241        _ => "top-0 right-0",
242    };
243    let mut icon_color = props.icon_type;
244    if props.icon_color.is_empty() {
245        icon_color = match &props.icon_type.to_lowercase()[..] {
246            "warning" => "#CC5500", // Burnt Orange
247            "error" => "#EE4B2B",   // Bright Red
248            "success" => "lightgreen",
249            "info" => "lightblue",
250            "question" => "lightgray",
251            _ => props.icon_type,
252        };
253    }
254    // SVGs taken from: https://fontawesome.com/icons
255    let icon_path = match &props.icon_type.to_lowercase()[..] {
256        "warning" => {
257            html! {
258                <svg
259                    xmlns="http://www.w3.org/2000/svg"
260                    width={props.icon_width}
261                    class="p-2 m-2"
262                    fill={icon_color}
263                    viewBox="0 0 512 512"
264                >
265                    <path
266                        d="M248.4 84.3c1.6-2.7 4.5-4.3 7.6-4.3s6 1.6 7.6 4.3L461.9 410c1.4 2.3 2.1 4.9 2.1 7.5c0 8-6.5 14.5-14.5 14.5H62.5c-8 0-14.5-6.5-14.5-14.5c0-2.7 .7-5.3 2.1-7.5L248.4 84.3zm-41-25L9.1 385c-6 9.8-9.1 21-9.1 32.5C0 452 28 480 62.5 480h387c34.5 0 62.5-28 62.5-62.5c0-11.5-3.2-22.7-9.1-32.5L304.6 59.3C294.3 42.4 275.9 32 256 32s-38.3 10.4-48.6 27.3zM288 368a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm-8-184c0-13.3-10.7-24-24-24s-24 10.7-24 24v96c0 13.3 10.7 24 24 24s24-10.7 24-24V184z"
267                    />
268                </svg>
269            }
270        }
271        "error" => {
272            html! {
273                <svg
274                    xmlns="http://www.w3.org/2000/svg"
275                    width={props.icon_width}
276                    class="p-2 m-2"
277                    fill={icon_color}
278                    viewBox="0 0 20 20"
279                >
280                    <path
281                        d="M12.71,7.291c-0.15-0.15-0.393-0.15-0.542,0L10,9.458L7.833,7.291c-0.15-0.15-0.392-0.15-0.542,0c-0.149,0.149-0.149,0.392,0,0.541L9.458,10l-2.168,2.167c-0.149,0.15-0.149,0.393,0,0.542c0.15,0.149,0.392,0.149,0.542,0L10,10.542l2.168,2.167c0.149,0.149,0.392,0.149,0.542,0c0.148-0.149,0.148-0.392,0-0.542L10.542,10l2.168-2.168C12.858,7.683,12.858,7.44,12.71,7.291z M10,1.188c-4.867,0-8.812,3.946-8.812,8.812c0,4.867,3.945,8.812,8.812,8.812s8.812-3.945,8.812-8.812C18.812,5.133,14.867,1.188,10,1.188z M10,18.046c-4.444,0-8.046-3.603-8.046-8.046c0-4.444,3.603-8.046,8.046-8.046c4.443,0,8.046,3.602,8.046,8.046C18.046,14.443,14.443,18.046,10,18.046z"
282                    />
283                </svg>
284            }
285        }
286        "success" => {
287            html! {
288                <svg
289                    xmlns="http://www.w3.org/2000/svg"
290                    width={props.icon_width}
291                    class="p-2 m-2"
292                    fill={icon_color}
293                    viewBox="0 0 512 512"
294                >
295                    <path
296                        d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"
297                    />
298                </svg>
299            }
300        }
301        "info" => {
302            html! {
303                <svg
304                    xmlns="http://www.w3.org/2000/svg"
305                    width={props.icon_width}
306                    class="p-2 m-2"
307                    fill={icon_color}
308                    viewBox="0 0 16 16"
309                >
310                    <path
311                        d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
312                    />
313                    <path
314                        d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"
315                    />
316                </svg>
317            }
318        }
319        "question" => {
320            html! {
321                <svg
322                    xmlns="http://www.w3.org/2000/svg"
323                    width={props.icon_width}
324                    class="p-2 m-2"
325                    fill={icon_color}
326                    viewBox="0 0 16 16"
327                >
328                    <path
329                        d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
330                    />
331                    <path
332                        d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"
333                    />
334                </svg>
335            }
336        }
337        _ => {
338            html! {
339                <svg
340                    xmlns="http://www.w3.org/2000/svg"
341                    width={props.icon_width}
342                    class="p-2 m-2"
343                    fill={icon_color}
344                    viewBox="0 0 16 16"
345                >
346                    <path
347                        d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
348                    />
349                    <path
350                        d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"
351                    />
352                </svg>
353            }
354        }
355    };
356
357    html! {
358        if show {
359            <div
360                class={format!("top-0 left-0 fixed p-3 z-10 transition duration-300 ease-in bg-gray-900 bg-opacity-75 w-screen h-screen {}", props.container_class)}
361            >
362                <div
363                    class={format!("absolute items-center {} {}", props.alert_class, position_class)}
364                >
365                    { if props.show_close_button {
366                        html! {
367                            <button type="button" class="absolute top-0 right-0 p-2 m-4 text-white bg-black border rounded-xl border-2" onclick={on_cancel.clone()}>{"x"}</button>
368                        }
369                    } else {
370                        html! {}
371                    } }
372                    <div class={props.icon_class}>{ icon_path }</div>
373                    <strong class={props.title_class}>{ props.title }</strong>
374                    <hr class="my-2 border-gray-600" />
375                    <p class={props.message_class}>{ props.message }</p>
376                    { if props.show_confirm_button {
377                        html! {
378                            <button class={props.confirm_button_class} onclick={on_confirm}>
379                                {props.confirm_button_text}
380                            </button>
381                        }
382                    } else {
383                        html! {}
384                    } }
385                    { if props.show_cancel_button {
386                        html! {
387                            <button class={props.cancel_button_class} onclick={on_cancel.clone()}>
388                                {props.cancel_button_text}
389                            </button>
390                        }
391                    } else {
392                        html! {}
393                    } }
394                </div>
395            </div>
396        }
397    }
398}