yew_sidebar/
lib.rs

1#![doc(
2    html_logo_url = "https://github.com/next-rs/yew-sidebar/assets/62179149/94fb2191-884e-4643-a794-d8e5c459f5d6",
3    html_favicon_url = "https://github.com/next-rs/yew-sidebar/assets/62179149/e0c325e5-16b8-439d-b7c2-48b2ffe8476d"
4)]
5
6//! # Yew Sidebar - Documentation
7//!
8//! Welcome to the official Yew Sidebar documentation. This library
9//! provides a flexible and customizable sidebar component for your Yew applications.
10//!
11//! ## Usage
12//!
13//! To use the Yew Sidebar library, add the following dependency to your `Cargo.toml` file:
14//!
15//! ```sh
16//! cargo add yew-sidebar
17//! ```
18//!
19//! To integrate the library into your Yew application, you can use the `Sidebar` component.
20//! Here's a simple example of how to use it:
21//!
22//! ```rust,no_run
23//! use yew::prelude::*;
24//! use yew_sidebar::{Sidebar, SidebarProps, MenuItem};
25//!
26//! // Your Yew component structure here...
27//!
28//! #[function_component]
29//! pub fn MySidebarComponent() -> Html {
30//!     // Your component logic here...
31//!
32//!     let menu_items = vec![
33//!         MenuItem {
34//!             icon: html! { /* Your icon HTML here */ },
35//!             text: "Home",
36//!             link: "/home",
37//!             class: "your-menu-item-class",
38//!             title: "Your Menu Title",
39//!             submenus: vec![], // Nested MenuItem structures if needed
40//!         },
41//!         // Add more MenuItem structures as needed...
42//!     ];
43//!
44//!     let sidebar_props = SidebarProps {
45//!         fixed: false,
46//!         sider_collapsed: false,
47//!         title: "Your Sidebar Title",
48//!         menu_items,
49//!         // Customize other props as needed...
50//!         // ...
51//!         ..SidebarProps::default()
52//!     };
53//!
54//!     html! {
55//!         <Sidebar ..sidebar_props />
56//!     }
57//! }
58//! ```
59//!
60//! For more detailed information, check the [examples] provided in the library.
61//!
62//! [examples]: https://github.com/next-rs/yew-sidebar/examples
63//!
64//! ## Configuration
65//!
66//! Yew Sidebar allows you to configure various aspects of the sidebar through the `SidebarProps`
67//! structure. You can customize properties such as width, padding, display, styling, icons, and more.
68//! Refer to the `SidebarProps` documentation for detailed configuration options.
69//!
70//! ```rust,no_run
71//! use yew_sidebar::{SidebarProps, MenuItem};
72//! use yew::prelude::*;
73//!
74//! let menu_items = vec![
75//!     MenuItem {
76//!         icon: html! { /* Your icon HTML here */ },
77//!         text: "Home",
78//!         link: "/home",
79//!         class: "your-menu-item-class",
80//!         title: "Your Menu Title",
81//!         submenus: vec![], // Nested MenuItem structures if needed
82//!     },
83//!     // Add more MenuItem structures as needed...
84//! ];
85//!
86//! let sidebar_props = SidebarProps {
87//!     fixed: false,
88//!     sider_collapsed: false,
89//!     title: "Your Sidebar Title",
90//!     menu_items,
91//!     // Customize other props as needed...
92//!     // ...
93//!     ..SidebarProps::default()
94//! };
95//!
96//! let sidebar_component = html! {
97//!     <Sidebar ..sidebar_props />
98//! };
99//! ```
100//!
101//! ## Contribution
102//!
103//! If you encounter any issues or have suggestions for improvements, feel free to contribute
104//! to the [GitHub repository](https://github.com/next-rs/yew-sidebar). We appreciate your feedback
105//! and involvement in making Yew Sidebar better!
106//!
107//! ## Acknowledgments
108//!
109//! Special thanks to the Yew community and contributors for such an amazing framework.
110
111use yew::prelude::*;
112use yew_accordion::{Accordion, AccordionButton};
113
114const WIDTH_COLLAPSED: &'static str = "w-16";
115const WIDTH_EXPANDED: &'static str = "w-64";
116const PADDING_COLLAPSED: &'static str = "p-2";
117const PADDING_EXPANDED: &'static str = "p-4";
118const DISPLAY_COLLAPSED: &'static str = "hidden";
119const DISPLAY_EXPANDED: &'static str = "flex";
120const JUSTIFY_CONTENT: &'static str = "justify-start";
121const ALIGN_ITEMS: &'static str = "items-center";
122const HEIGHT: &'static str = "h-screen";
123const BACKGROUND_COLOR: &'static str = "bg-gray-800";
124const FONT: &'static str = "text-black";
125const ICON_COLOR: &'static str = "white";
126const BUTTON_BORDER_RADIUS: &'static str = "rounded";
127const BUTTON_BACKGROUND_COLOR: &'static str = "bg-blue-600";
128const BUTTON_WIDTH: &'static str = "w-12";
129const BUTTON_HEIGHT: &'static str = "h-12";
130const MENU_ITEM: &'static str = "\
131    text-gray-300 \
132    hover:bg-gray-800 \
133    hover:text-white \
134    flex \
135    items-center \
136    space-x-2 \
137    p-1 \
138    rounded \
139    transition duration-300 \
140";
141const LOGO_CLASS: &str = "flex items-center";
142const LOGO_IMG_CLASS: &str = "w-32 md:w-40";
143
144#[derive(Properties, Clone, PartialEq)]
145pub struct SidebarProps {
146    // General Props
147    #[prop_or(false)]
148    pub fixed: bool,
149    #[prop_or(false)]
150    pub sider_collapsed: bool,
151    #[prop_or_default]
152    pub menu_items: Vec<MenuItem>,
153    // Prop for toggle icon when collapsed
154    #[prop_or_default]
155    pub toggle_icon_collapsed: Html,
156    // Prop for toggle icon when expanded
157    #[prop_or_default]
158    pub toggle_icon_expanded: Html,
159
160    // Layout Props
161    // Prop for width of collapsed state
162    #[prop_or(WIDTH_COLLAPSED)]
163    pub width_collapsed: &'static str,
164    // Prop for width of expanded state
165    #[prop_or(WIDTH_EXPANDED)]
166    pub width_expanded: &'static str,
167    // Prop for padding of collapsed state
168    #[prop_or(PADDING_COLLAPSED)]
169    pub padding_collapsed: &'static str,
170    // Prop for padding of expanded state
171    #[prop_or(PADDING_EXPANDED)]
172    pub padding_expanded: &'static str,
173    // Prop for display of collapsed state
174    #[prop_or(DISPLAY_COLLAPSED)]
175    pub display_collapsed: &'static str,
176    // Prop for display of expanded state
177    #[prop_or(DISPLAY_EXPANDED)]
178    pub display_expanded: &'static str,
179    // Prop for justify-content
180    #[prop_or(JUSTIFY_CONTENT)]
181    pub justify_content: &'static str,
182    // Prop for align-items
183    #[prop_or(ALIGN_ITEMS)]
184    pub align_items: &'static str,
185    // Prop for height
186    #[prop_or(HEIGHT)]
187    pub height: &'static str,
188
189    // Style Props
190    // Prop for background color
191    #[prop_or(BACKGROUND_COLOR)]
192    pub background_color: &'static str,
193    // Prop for font
194    #[prop_or(FONT)]
195    pub font: &'static str,
196    // Prop for icon color
197    #[prop_or(ICON_COLOR)]
198    pub icon_color: &'static str,
199    // Prop for button border-radius
200    #[prop_or(BUTTON_BORDER_RADIUS)]
201    pub button_border_radius: &'static str,
202    // Prop for button background color
203    #[prop_or(BUTTON_BACKGROUND_COLOR)]
204    pub button_background_color: &'static str,
205    // Prop for button width
206    #[prop_or(BUTTON_WIDTH)]
207    pub button_width: &'static str,
208    // Prop for button height
209    #[prop_or(BUTTON_HEIGHT)]
210    pub button_height: &'static str,
211
212    // Title Props
213    // Prop for title
214    #[prop_or_default]
215    pub title: &'static str,
216
217    // Logo Props
218    #[prop_or("images/logo.png")]
219    pub logo_src: &'static str,
220    #[prop_or("logo")]
221    pub logo_alt: &'static str,
222    #[prop_or(LOGO_IMG_CLASS)]
223    pub logo_img_class: &'static str,
224    #[prop_or("/")]
225    pub logo_link: &'static str,
226    #[prop_or(LOGO_CLASS)]
227    pub logo_class: &'static str,
228
229    // Bottom section props
230    #[prop_or_default]
231    pub bottom_section: Html,
232
233    /// Properties for the Accordion components, aka submenus.
234    #[prop_or_default]
235    /// Size of the accordion. Possible values: "sm", "md", "lg".
236    pub size: &'static str,
237    #[prop_or_default]
238    /// ARIA controls attribute for accessibility.
239    pub aria_controls: &'static str,
240    #[prop_or_default]
241    /// Class for the container element.
242    pub container_class: &'static str,
243    #[prop_or_default]
244    /// Class for the expanded element.
245    pub expanded_element_class: &'static str,
246    #[prop_or_default]
247    /// Class for the collapsed element.
248    pub collapsed_element_class: &'static str,
249    #[prop_or_default]
250    /// Class for the content container.
251    pub content_container_class: &'static str,
252}
253
254impl Default for SidebarProps {
255    fn default() -> Self {
256        Self {
257            fixed: false,
258            sider_collapsed: false,
259            title: "",
260            menu_items: Vec::new(),
261            width_collapsed: WIDTH_COLLAPSED,
262            width_expanded: WIDTH_EXPANDED,
263            padding_collapsed: PADDING_COLLAPSED,
264            padding_expanded: PADDING_EXPANDED,
265            display_collapsed: DISPLAY_COLLAPSED,
266            display_expanded: DISPLAY_EXPANDED,
267            justify_content: JUSTIFY_CONTENT,
268            align_items: ALIGN_ITEMS,
269            height: HEIGHT,
270            background_color: BACKGROUND_COLOR,
271            font: FONT,
272            icon_color: ICON_COLOR,
273            button_border_radius: BUTTON_BORDER_RADIUS,
274            button_background_color: BUTTON_BACKGROUND_COLOR,
275            button_width: BUTTON_WIDTH,
276            button_height: BUTTON_HEIGHT,
277            logo_src: "images/logo.png",
278            logo_alt: "logo",
279            logo_img_class: LOGO_CLASS,
280            logo_link: "/",
281            logo_class: LOGO_CLASS,
282            toggle_icon_collapsed: html! {},
283            toggle_icon_expanded: html! {},
284            bottom_section: html! {},
285            size: "md",
286            aria_controls: "accordion",
287            container_class: "text-white",
288            expanded_element_class: "text-white",
289            collapsed_element_class: "text-white",
290            content_container_class: "text-white",
291        }
292    }
293}
294
295#[function_component(Sidebar)]
296pub fn sidebar(props: &SidebarProps) -> Html {
297    let is_collapsed_handle = use_state(|| props.sider_collapsed);
298    let is_collapsed = (*is_collapsed_handle).clone();
299
300    html! {
301        <>
302            { if props.fixed {
303                html! {
304                    <div
305                        class={format!("transition-all duration-200 {}",
306                            if is_collapsed { props.width_collapsed }
307                            else { props.width_expanded })
308                        }
309                    />
310                }
311            } else {
312                html! {}
313            } }
314            <div
315                class={format!(
316                    "{} {} {} {} {} {} {} {}",
317                    if is_collapsed { props.width_collapsed } else { props.width_expanded },
318                    if is_collapsed { props.padding_collapsed } else { props.padding_expanded },
319                    props.display_expanded,
320                    props.justify_content,
321                    props.align_items,
322                    props.height,
323                    props.background_color,
324                    props.font,
325                )}
326            >
327                { render_logo_and_title(&props, is_collapsed_handle) }
328                { render_menu(&props, is_collapsed) }
329                { props.bottom_section.clone() }
330            </div>
331        </>
332    }
333}
334
335fn render_logo_and_title(props: &SidebarProps, is_collapsed_handle: UseStateHandle<bool>) -> Html {
336    let on_toggle = {
337        let is_collapsed_handle = is_collapsed_handle.clone();
338        Callback::from(move |_| {
339            is_collapsed_handle.set(!*is_collapsed_handle);
340        })
341    };
342    let props_clone = props.clone();
343    html! {
344        <div class="flex items-center">
345            <button
346                type="button"
347                class={format!(
348                    "{} {} {} {}",
349                    props.button_border_radius,
350                    props.button_background_color,
351                    props.button_width,
352                    props.button_height,
353                )}
354                onclick={on_toggle}
355            >
356                { if *is_collapsed_handle {
357                    props_clone.toggle_icon_collapsed
358                } else {
359                    props_clone.toggle_icon_expanded
360                } }
361            </button>
362            if !*is_collapsed_handle && !props.logo_src.is_empty() { { render_logo(&props) } }
363            if !*is_collapsed_handle { <span class="ml-2 text-white">{ props.title }</span> }
364        </div>
365    }
366}
367
368fn render_logo(props: &SidebarProps) -> Html {
369    html! {
370        <div id="logo" class={props.logo_class}>
371            <a href={props.logo_link} class="nav-link">
372                <img src={props.logo_src} alt={props.logo_alt} class={props.logo_img_class} />
373            </a>
374        </div>
375    }
376}
377
378fn render_menu(props: &SidebarProps, is_collapsed: bool) -> Html {
379    html! {
380        <ul>
381            { for props.menu_items.iter().map(|item| render_menu_item(&props, item, is_collapsed)) }
382        </ul>
383    }
384}
385
386fn render_menu_item(props: &SidebarProps, item: &MenuItem, is_collapsed: bool) -> Html {
387    let submenu_html = if !item.submenus.is_empty() {
388        html! {
389            <Accordion
390                expanded_element={html! {
391                    <AccordionButton class={"text-white"}>
392                        <div class="flex">
393                            { item.icon.clone() }
394                            if !is_collapsed { <span class="ml-2">{ &item.text }</span> }
395                        </div>
396                    </AccordionButton>
397                }}
398                collapsed_element={html! {
399                    <AccordionButton class={"text-white"}>
400                        <div class="flex">
401                            { item.icon.clone() }
402                            if !is_collapsed { <span class="ml-2">{ &item.text }</span> }
403                        </div>
404                    </AccordionButton>
405                }}
406                size={props.size}
407                aria_controls={props.aria_controls}
408                container_class={props.container_class}
409                expanded_element_class={props.expanded_element_class}
410                collapsed_element_class={props.collapsed_element_class}
411                content_container_class={props.content_container_class}
412            >
413                <ul>
414                    { for item.submenus.iter().map(|submenu| render_menu_item(&props, submenu, is_collapsed)) }
415                </ul>
416            </Accordion>
417        }
418    } else {
419        html! {
420            <>{ item.icon.clone() }if !is_collapsed { <span class="ml-2">{ &item.text }</span> }</>
421        }
422    };
423    html! {
424        <li class={item.class}>
425            <div>{ item.title }</div>
426            <a href={item.link} class={MENU_ITEM}>{ submenu_html }</a>
427        </li>
428    }
429}
430
431#[derive(Clone, Properties, PartialEq)]
432pub struct MenuItem {
433    #[prop_or_default]
434    pub icon: Html,
435    #[prop_or_default]
436    pub text: &'static str,
437    #[prop_or_default]
438    pub link: &'static str,
439    #[prop_or_default]
440    pub class: &'static str,
441    #[prop_or_default]
442    pub title: &'static str,
443    #[prop_or_default]
444    pub submenus: Vec<MenuItem>,
445}