yew_bootstrap/component/
accordion.rs

1use std::rc::Rc;
2
3use yew::prelude::*;
4
5/// # Properties of [AccordionHeader]
6#[derive(Properties, Clone, PartialEq)]
7struct AccordionHeaderProps {
8    /// The html id of this component
9    #[prop_or_default]
10    heading_id: AttrValue,
11
12    /// The title displayed in the header
13    #[prop_or_default]
14    title: AttrValue,
15
16    /// Classes attached to the button holding the title
17    #[prop_or_default]
18    button_classes: Classes,
19
20    /// The html id of associated collapse for this [AccordionItem]
21    #[prop_or_default]
22    collapse_id: AttrValue,
23
24    /// If the associated accordion collapse is open
25    #[prop_or_default]
26    expanded: bool
27}
28
29/// # Accordion Header
30/// Used with [crate::component::AccordionItem] to create accordion drop downs
31/// This represents the title of the accordion item that is always visible
32/// 
33/// See [AccordionHeaderProps] for a listing of properties
34///
35/// This component is not meant to be used stand-alone as it's only rendered inside of Accordions
36#[function_component]
37fn AccordionHeader(props: &AccordionHeaderProps) -> Html {
38    html! { 
39        <h2 class="accordion-header" id={props.heading_id.clone()}>
40            <button
41                class={props.button_classes.clone()} 
42                type="button" 
43                data-bs-toggle="collapse" 
44                data-bs-target={format!("#{}", props.collapse_id)} 
45                aria-expanded={props.expanded.to_string()} 
46                aria-controls={props.collapse_id.clone()}
47            >
48                { props.title.clone() }
49            </button>
50        </h2>
51    }
52}
53
54/// # Properties of [AccordionCollapse]
55#[derive(Properties, Clone, PartialEq)]
56struct AccordionCollapseProps {
57    /// Parent [Accordion] html id attribute
58    #[prop_or(AttrValue::from("main-accordion"))]
59    parent_id: AttrValue,
60
61    /// Html id of this component
62    #[prop_or_default]
63    collapse_id: AttrValue,
64
65    /// Html id of associated header for this [AccordionItem]
66    #[prop_or_default]
67    heading_id: AttrValue,
68
69    /// Opening this item will close other items in the [Accordion]
70    #[prop_or_default]
71    stay_open: bool,
72
73    /// Classes attached to the div
74    #[prop_or_default]
75    class: Classes,
76
77    /// Inner components
78    #[prop_or_default]
79    children: Children,
80}
81
82/// # Accordion Collapse
83/// Used with [crate::component::AccordionItem] to create accordion drop downs
84/// This represents the body of the accordion item that can be opened/closed
85/// 
86/// See [AccordionCollapseProps] for a listing of properties
87///
88/// This component is not meant to be used stand-alone as it's only rendered inside of Accordions
89#[function_component]
90fn AccordionCollapse(props: &AccordionCollapseProps) -> Html {
91    if props.stay_open {
92        return html! {
93            <div id={props.collapse_id.clone()} class={props.class.clone()} aria-labelledby={props.heading_id.clone()}>
94                { for props.children.iter() }
95            </div>
96        }
97    }
98    html! {
99        <div id={props.collapse_id.clone()} class={props.class.clone()} aria-labelledby={props.heading_id.clone()} data-bs-parent={format!("#{}", props.parent_id)}>
100            { for props.children.iter() }
101        </div>
102    }
103}
104
105/// # Properties of [AccordionItem]
106#[derive(Properties, Clone, PartialEq)]
107pub struct AccordionItemProps {
108    /// Text displayed in this items heading
109    #[prop_or_default]
110    pub title: AttrValue,
111
112    /// Item is currently open
113    #[prop_or_default]
114    pub expanded: bool,
115
116    /// Inner components (displayed in the [AccordionCollapse])
117    #[prop_or_default]
118    pub children: Children,
119
120    /// Opening this item doesn't close other items
121    #[prop_or_default]
122    stay_open: bool,
123
124    /// Html id attribute of parent [Accordion]
125    #[prop_or(AttrValue::from("main-accordion"))]
126    parent_id: AttrValue,
127
128    /// Position in the parent [Accordion]
129    #[prop_or_default]
130    item_id: usize,
131}
132
133/// # A singular accordion item, child of [Accordion]
134/// Used as a child of [Accordion] to create an accordion menu.
135/// 
136/// Child components will be displayed in the body of the accordion item
137#[function_component]
138pub fn AccordionItem(props: &AccordionItemProps) -> Html {
139    let heading_id = format!("{}-heading-{}", props.parent_id, props.item_id);
140    let collapse_id = format!("{}-collapse-{}", props.parent_id, props.item_id);
141
142    let mut button_classes = classes!("accordion-button");
143    let mut collapse_classes = classes!("accordion-collapse",  "collapse");
144
145    // TODO: Maybe hook up the `expanded` property to some state depending on `stay_open`
146    //
147    // I think in the bootstrap docs this is really only meant to show one item as expanded after loading the page
148    // However as it currently is, users may be able to set this on multiple items at once
149    // This is probably fine during initial page load since they can be closed individually
150    // But it acts weird if an end-user were to open another item as it would close all of them unless `stay_open` is true
151    // 
152    // Additionally if some other part of the page is setup to use state to open an item
153    // This will cause 2 items to be open at once even if the `stay_open` flag is false
154    // There's no real harm putting the closing of accordion items on the user, but it would be nice if there were
155    // some sort of built in way to handle this
156    //
157    // I use ssr in my project so ideally this would also not interfere with rendering server side
158    if !props.expanded {
159        button_classes.push("collapsed");
160    } else {
161        collapse_classes.push("show");
162    }
163
164    html! {
165        <div class="accordion-item">
166            <AccordionHeader 
167                title={props.title.clone()}
168                heading_id={heading_id.clone()}
169                button_classes={button_classes}
170                collapse_id={collapse_id.clone()}
171                expanded={props.expanded}
172            />
173            <AccordionCollapse
174                class={collapse_classes}
175                stay_open={props.stay_open}
176                heading_id={heading_id}
177                collapse_id={collapse_id.clone()}
178                parent_id={props.parent_id.clone()}
179            >
180                <div class="accordion-body">
181                    { for props.children.iter() }
182                </div>
183            </AccordionCollapse>
184        </div>
185    }
186}
187
188/// # Properties of [Accordion]
189#[derive(Properties, Clone, PartialEq)]
190pub struct AccordionProps {
191    /// Html id of the accordion - should be unique within it's page
192    #[prop_or(AttrValue::from("main-accordion"))]
193    pub id: AttrValue,
194
195    /// Accordion is flush with the container and removes some styling elements
196    #[prop_or_default]
197    pub flush: bool,
198
199    /// Opening an item won't close other items in the accordion
200    #[prop_or_default]
201    pub stay_open: bool,
202
203    // The [AccordionItem] instances controlled by this accordion
204    #[prop_or_default]
205    pub children: ChildrenWithProps<AccordionItem>,
206}
207
208/// # Accordion
209/// [Accordion] is used to group several [crate::component::AccordionItem] instances together.
210/// 
211/// See [AccordionProps] for a listing of properties.
212/// 
213/// See [bootstrap docs](https://getbootstrap.com/docs/5.0/components/accordion/) for a full demo of accordions
214/// 
215/// Basic example of using an Accordion
216/// 
217/// ```rust
218/// use yew::prelude::*;
219/// use yew_bootstrap::component::{Accordion, AccordionItem};
220/// fn test() -> Html {
221///     html!{
222///         <Accordion>
223///             <AccordionItem title={"Heading 1"}>
224///                 <p>{"Some text inside "}<strong>{"THE BODY"}</strong>{" of the accordion item"}</p>
225///             </AccordionItem>
226///             <AccordionItem title={"Heading 2"}>
227///                 <h3>{"Some other text under another accordion"}</h3>
228///                 <button>{"Button with some functionality"}</button>
229///             </AccordionItem>
230///         </Accordion>
231///     }
232/// }
233/// ```
234/// 
235/// 
236/// Example of using an Accordion while mapping a list to AccordionItem children
237/// 
238/// ```rust
239/// use yew::{prelude::*, virtual_dom::VChild};
240/// use yew_bootstrap::component::{Accordion, AccordionItem};
241/// fn test() -> Html {
242///     let items = vec![("title1", "body1"), ("title2", "body2")];
243///     html! {
244///         <Accordion id="features-and-challenges">
245///             {
246///                 items.iter().map(|item| {
247///                     html_nested! {
248///                         <AccordionItem title={item.0.clone()}>
249///                             {item.0.clone()}
250///                         </AccordionItem>
251///                     }
252///                 }).collect::<Vec<VChild<AccordionItem>>>()
253///             }
254///         </Accordion>
255///     }
256/// }
257/// ```
258#[function_component]
259pub fn Accordion(props: &AccordionProps) -> Html {
260    let mut classes = classes!("accordion");
261    if props.flush {
262        classes.push("accordion-flush");
263    }
264
265    html! {
266        <div class={classes} id={props.id.clone()}>
267            {
268                for props.children.iter().enumerate().map(|(index, mut child)| {
269                    let child_props = Rc::make_mut(&mut child.props);
270                    child_props.item_id = index;
271                    child_props.parent_id = props.id.clone();
272                    child_props.stay_open = props.stay_open;
273                    child
274                })
275            }
276        </div>
277    }
278}