vertigo_forms/tabs/
mod.rs

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