Skip to main content

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    #[allow(dead_code)]
43    on_hide: OnHide,
44}
45
46/// # Header for a [Modal] dialog
47/// See [ModalHeaderProps] for a listing of properties
48pub struct ModalHeader { }
49
50/// # Body for a [Modal] dialog
51/// See [ModalBodyProps] for a listing of properties
52pub struct ModalBody { }
53
54/// # Footer for a [Modal] dialog
55/// See [ModalFooterProps] for a listing of properties
56pub struct ModalFooter { }
57
58/// Properties for [ModalFooter]
59#[derive(Properties, Clone, PartialEq)]
60pub struct ModalFooterProps {
61    #[prop_or_default]
62    pub children: Children
63}
64
65impl Component for ModalFooter {
66    type Message = ();
67    type Properties = ModalFooterProps;
68
69    fn create(_ctx: &Context<Self>) -> Self {
70        Self {}
71    }
72
73    fn view(&self, ctx: &Context<Self>) -> Html {
74        let props = ctx.props();
75
76        html! {
77            <div class="modal-footer">
78                { for props.children.iter() }
79            </div>
80        }
81    }
82}
83
84/// Properties for [ModalHeader]
85#[derive(Properties, Clone, PartialEq)]
86pub struct ModalHeaderProps {
87    /// Title for the Modal dialog
88    #[prop_or_default]
89    pub title: String,
90
91    /// required for triggering open/close
92    #[prop_or_default]
93    pub id: String,
94}
95
96impl Component for ModalHeader {
97    type Message = ();
98    type Properties = ModalHeaderProps;
99
100    fn create(_ctx: &Context<Self>) -> Self {
101        Self {}
102    }
103
104    fn view(&self, ctx: &Context<Self>) -> Html {
105        let props = ctx.props();
106
107        html! {
108            <div class="modal-header">
109                <h5 class="modal-title" id={format!("#{}", props.id.clone())}>{props.title.clone()}</h5>
110                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
111            </div>
112        }
113    }
114}
115
116/// Properties for [ModalBody]
117#[derive(Properties, Clone, PartialEq)]
118pub struct ModalBodyProps {
119    #[prop_or_default]
120    pub children: Children
121}
122
123impl Component for ModalBody {
124    type Message = ();
125    type Properties = ModalBodyProps;
126
127    fn create(_ctx: &Context<Self>) -> Self {
128        Self {}
129    }
130
131    fn view(&self, ctx: &Context<Self>) -> Html {
132        let props = ctx.props();
133
134        html! {
135            <div class="modal-body">
136                { for props.children.iter() }
137            </div>
138        }
139    }
140}
141
142pub struct OnHide {
143    #[allow(dead_code)]
144    listener: Option<gloo_events::EventListener>,
145}
146
147impl OnHide {
148    pub fn new(target: &EventTarget, callback: Option<Callback<Event>>) -> Self {
149        let Some(callback) = callback else {
150            return Self { listener: None };
151        };
152
153        let listener = {
154            let option = EventListenerOptions::enable_prevent_default();
155
156            Some(gloo_events::EventListener::new_with_options(target, "hide.bs.modal", option, move |_event| {
157                callback.emit(_event.clone());
158            }))
159        };
160
161        Self { listener }
162    }
163}
164
165/// Properties for Modal
166#[derive(Properties, Clone, PartialEq)]
167pub struct ModalProps {
168    #[prop_or_default]
169    pub title: String,
170    /// required for triggering open/close
171    #[prop_or_default]
172    pub id: String,
173    /// modal body, typically [ModalHeader], [ModalBody] or [ModalFooter]
174    #[prop_or_default]
175    pub children: Children,
176    /// Size of the modal
177    #[prop_or_default]
178    pub size: ModalSize,
179    /// Function to be called on the 'hide.bs.modal' event, takes no parameters
180    #[prop_or_default]
181    pub on_hide: Option<Callback<Event>>,
182}
183
184impl Component for Modal {
185    type Message = ();
186    type Properties = ModalProps;
187
188    fn create(_ctx: &Context<Self>) -> Self {
189        let body = body();
190        Self { on_hide: OnHide::new(
191            &body,
192            _ctx.props().on_hide.clone(),
193        )}
194    }
195
196    fn view(&self, ctx: &Context<Self>) -> Html {
197        let props = ctx.props();
198
199        let mut dialog_classes = Classes::new();
200        dialog_classes.push("modal-dialog");
201
202        match props.size {
203            ModalSize::ExtraLarge => dialog_classes.push("modal-xl"),
204            ModalSize::Large => dialog_classes.push("modal-lg"),
205            ModalSize::Small => dialog_classes.push("modal-sm"),
206            _ => (),
207        }
208
209        html! {
210            <div class="modal" tabindex="-1" id={props.id.clone()}>
211                <div class={dialog_classes}>
212                    <div class="modal-content">
213                        { for props.children.iter() }
214                    </div>
215                </div>
216            </div>
217        }
218    }
219}