yew_bootstrap/component/
modal.rs

1use web_sys::EventTarget;
2use yew::prelude::*;
3use gloo_events::EventListenerOptions;
4use gloo_utils::body;
5
6/// Represents the optional size of a Modal dialog, described [here](https://getbootstrap.com/docs/5.1/components/modal/#optional-sizes)
7#[derive(Default, Clone, PartialEq, Eq)]
8pub enum ModalSize {
9    ExtraLarge,
10    Large,
11    #[default]
12    Normal,
13    Small,
14}
15
16/// # Modal dialog
17/// Modal dialog, parent of [ModalHeader], [ModalBody] and [ModalFooter].
18/// 
19/// See [ModalProps] for a listing of properties
20/// 
21/// ## Example
22/// ```rust
23/// use yew::prelude::*;
24/// use yew_bootstrap::component::{Modal, ModalHeader, ModalBody, ModalFooter, Button, ModalSize};
25/// use yew_bootstrap::util::Color;
26/// fn test() -> Html {
27///     html!{
28///         <Modal id="ExampleModal" size={ModalSize::Large}> // size defaults to Normal
29///             <ModalHeader title="Modal title" id="ExampleModal"/>
30///             <ModalBody>
31///                 <p>{"Modal body text goes here."}</p>
32///             </ModalBody>
33///             <ModalFooter>
34///                 <Button style={ Color::Secondary } modal_dismiss={ true }>{ "Close" }</Button>
35///                 <Button style={ Color::Primary }>{ "Save changes" }</Button>
36///             </ModalFooter>
37///         </Modal>
38///     }
39/// }
40/// ```
41pub struct Modal {
42    on_hide: OnHide,
43}
44
45/// # Header for a [Modal] dialog
46/// See [ModalHeaderProps] for a listing of properties
47pub struct ModalHeader { }
48
49/// # Body for a [Modal] dialog
50/// See [ModalBodyProps] for a listing of properties
51pub struct ModalBody { }
52
53/// # Footer for a [Modal] dialog
54/// See [ModalFooterProps] for a listing of properties
55pub struct ModalFooter { }
56
57/// Properties for [ModalFooter]
58#[derive(Properties, Clone, PartialEq)]
59pub struct ModalFooterProps {
60    #[prop_or_default]
61    pub children: Children
62}
63
64impl Component for ModalFooter {
65    type Message = ();
66    type Properties = ModalFooterProps;
67
68    fn create(_ctx: &Context<Self>) -> Self {
69        Self {}
70    }
71
72    fn view(&self, ctx: &Context<Self>) -> Html {
73        let props = ctx.props();
74
75        html! {
76            <div class="modal-footer">
77                { for props.children.iter() }
78            </div>
79        }
80    }
81}
82
83/// Properties for [ModalHeader]
84#[derive(Properties, Clone, PartialEq)]
85pub struct ModalHeaderProps {
86    /// Title for the Modal dialog
87    #[prop_or_default]
88    pub title: String,
89
90    /// required for triggering open/close
91    #[prop_or_default]
92    pub id: String,
93}
94
95impl Component for ModalHeader {
96    type Message = ();
97    type Properties = ModalHeaderProps;
98
99    fn create(_ctx: &Context<Self>) -> Self {
100        Self {}
101    }
102
103    fn view(&self, ctx: &Context<Self>) -> Html {
104        let props = ctx.props();
105
106        html! {
107            <div class="modal-header">
108                <h5 class="modal-title" id={format!("#{}", props.id.clone())}>{props.title.clone()}</h5>
109                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
110            </div>
111        }
112    }
113}
114
115/// Properties for [ModalBody]
116#[derive(Properties, Clone, PartialEq)]
117pub struct ModalBodyProps {
118    #[prop_or_default]
119    pub children: Children
120}
121
122impl Component for ModalBody {
123    type Message = ();
124    type Properties = ModalBodyProps;
125
126    fn create(_ctx: &Context<Self>) -> Self {
127        Self {}
128    }
129
130    fn view(&self, ctx: &Context<Self>) -> Html {
131        let props = ctx.props();
132
133        html! {
134            <div class="modal-body">
135                { for props.children.iter() }
136            </div>
137        }
138    }
139}
140
141pub struct OnHide {
142    listener: Option<gloo_events::EventListener>,
143}
144
145impl OnHide {
146    pub fn new(target: &EventTarget, callback: Option<Callback<(Event)>>) -> Self {
147        let Some(callback) = callback else {
148            return Self { listener: None };
149        };
150
151        let listener = {
152            let option = EventListenerOptions::enable_prevent_default();
153
154            Some(gloo_events::EventListener::new_with_options(target, "hide.bs.modal", option, move |_event| {
155                callback.emit(_event.clone());
156            }))
157        };
158
159        Self { listener }
160    }
161}
162
163/// Properties for Modal
164#[derive(Properties, Clone, PartialEq)]
165pub struct ModalProps {
166    #[prop_or_default]
167    pub title: String,
168    /// required for triggering open/close
169    #[prop_or_default]
170    pub id: String,
171    /// modal body, typically [ModalHeader], [ModalBody] or [ModalFooter]
172    #[prop_or_default]
173    pub children: Children,
174    /// Size of the modal
175    #[prop_or_default]
176    pub size: ModalSize,
177    /// Function to be called on the 'hide.bs.modal' event, takes no parameters
178    #[prop_or_default]
179    pub on_hide: Option<Callback<Event>>,
180}
181
182impl Component for Modal {
183    type Message = ();
184    type Properties = ModalProps;
185
186    fn create(_ctx: &Context<Self>) -> Self {
187        let body = body();
188        Self { on_hide: OnHide::new(
189            &body,
190            _ctx.props().on_hide.clone(),
191        )}
192    }
193
194    fn view(&self, ctx: &Context<Self>) -> Html {
195        let props = ctx.props();
196
197        let mut dialog_classes = Classes::new();
198        dialog_classes.push("modal-dialog");
199
200        match props.size {
201            ModalSize::ExtraLarge => dialog_classes.push("modal-xl"),
202            ModalSize::Large => dialog_classes.push("modal-lg"),
203            ModalSize::Small => dialog_classes.push("modal-sm"),
204            _ => (),
205        }
206
207        html! {
208            <div class="modal" tabindex="-1" id={props.id.clone()}>
209                <div class={dialog_classes}>
210                    <div class="modal-content">
211                        { for props.children.iter() }
212                    </div>
213                </div>
214            </div>
215        }
216    }
217}