Skip to main content

vertigo_forms/tabs/
mod.rs

1use std::rc::Rc;
2use vertigo::{
3    AttrGroup, Computed, Css, DomNode, Reactive, ToComputed, bind, component, css, dom, dom_element,
4};
5
6#[derive(Clone)]
7pub struct Tab<K> {
8    pub key: K,
9    pub name: String,
10    pub render: Rc<dyn Fn(&K) -> DomNode>,
11}
12
13#[derive(Clone)]
14pub struct TabsParams {
15    pub header_css: Css,
16    pub header_item_css: Css,
17    pub header_item_add_css: Css,
18    pub header_active_item_add_css: Css,
19    pub content_css: Css,
20    pub container_css: Css,
21}
22
23impl Default for TabsParams {
24    fn default() -> Self {
25        Self {
26            // render_header_item: None,
27            header_css: css! {"
28                display: flex;
29                flex-wrap: wrap;
30                gap: 10px;
31                margin: 0px;
32                padding: 0px;
33            "},
34            header_item_css: css! {"
35                cursor: pointer;
36            "},
37            header_item_add_css: Css::default(),
38            header_active_item_add_css: Css::default(),
39            content_css: Css::default(),
40            container_css: Css::default(),
41        }
42    }
43}
44
45/// [TabsHeader] and [TabsContent] rendered next to each other.
46// TODO: Add AttrGroups here for header and container, after https://github.com/vertigo-web/vertigo/issues/505 resolved
47#[component]
48pub fn Tabs<R, K>(current_tab: R, tabs: Vec<Tab<K>>, params: TabsParams)
49where
50    R: Reactive<K> + ToComputed<K> + Clone + 'static,
51    K: Clone + PartialEq + 'static,
52{
53    let current_computed = current_tab.to_computed();
54
55    dom! {
56        <div css={&params.container_css}>
57            <TabsHeader
58                {&current_tab}
59                tabs={tabs.clone()}
60                params={params.clone()}
61            />
62            <TabsContent
63                current_tab={current_computed}
64                tabs={tabs}
65                {params}
66            />
67        </div>
68    }
69}
70
71/// Nagivation bar for [TabsContent].
72#[component]
73pub fn TabsHeader<R, K>(
74    current_tab: R,
75    tabs: Vec<Tab<K>>,
76    params: TabsParams,
77    /// Any additional attributes for the header container
78    h: AttrGroup,
79) where
80    R: Reactive<K> + ToComputed<K> + Clone + 'static,
81    K: Clone + PartialEq + 'static,
82{
83    let header_item_css = params.header_item_css + params.header_item_add_css;
84    let header_active_item_add_css = params.header_active_item_add_css;
85
86    current_tab
87        .to_computed()
88        .render_value(move |current_tab_val| {
89            let header = dom_element! { <ul css={&params.header_css} {..h.clone()} /> };
90
91            tabs.iter().for_each(|tab| {
92                let on_click = bind!(current_tab, tab | _ | current_tab.set(tab.key.clone()));
93                let header_item_css = if current_tab_val == tab.key {
94                    &header_item_css + &header_active_item_add_css
95                } else {
96                    header_item_css.clone()
97                };
98                let item_css = css!("display: block;");
99                header.add_child(dom! {
100                    <li css={item_css}>
101                        <a  css={header_item_css} on_click={on_click}>{&tab.name}</a>
102                    </li>
103                });
104            });
105
106            header.into()
107        })
108}
109
110/// Renders content controlled by [TabsHeader] bar.
111#[component]
112pub fn TabsContent<K>(
113    current_tab: Computed<K>,
114    tabs: Vec<Tab<K>>,
115    params: TabsParams,
116    /// Any additional attributes for the content container
117    c: AttrGroup,
118) where
119    K: Clone + PartialEq + 'static,
120{
121    current_tab.render_value(move |current_tab| {
122        render_tab_content(&current_tab, &current_tab, &tabs, &params, &c)
123    })
124}
125
126/// Renders content controlled by [TabsHeader] bar,
127/// but allows to map groups of possible values to single tab,
128/// handy when using [Tabs] component connected with routing
129#[component]
130pub fn TabsContentMapped<K>(
131    current_tab: Computed<K>,
132    tabs: Vec<Tab<K>>,
133    tab_map: Rc<dyn Fn(K) -> K>,
134    params: TabsParams,
135    /// Any additional attributes for the content container
136    c: AttrGroup,
137) where
138    K: Clone + PartialEq + 'static,
139{
140    current_tab.render_value(move |current_tab| {
141        render_tab_content(
142            &current_tab,
143            &tab_map(current_tab.clone()),
144            &tabs,
145            &params,
146            &c,
147        )
148    })
149}
150
151fn render_tab_content<K: PartialEq + Clone>(
152    current_tab: &K,
153    effective_tab: &K,
154    tabs: &[Tab<K>],
155    params: &TabsParams,
156    c: &AttrGroup,
157) -> DomNode {
158    let inner = match tabs.iter().find(|tab| &tab.key == effective_tab).cloned() {
159        Some(tab) => (tab.render)(current_tab),
160        _ => dom! { <p>"Non-existent tab set"</p> },
161    };
162
163    dom! {
164        <div css={params.content_css.clone()} {..c}>
165            {inner}
166        </div>
167    }
168}