htmx_components/server/
table.rs

1use rscx::{component, html, props, CollectFragment};
2
3use super::html_element::HtmlElement;
4
5use rscx_web_macros::*;
6
7pub enum TableHeading {
8    Title(String),
9    Empty(String),
10}
11
12impl TableHeading {
13    pub fn title(title: impl Into<String>) -> TableHeading {
14        TableHeading::Title(title.into())
15    }
16    pub fn empty(sr_only_text: impl Into<String>) -> TableHeading {
17        TableHeading::Empty(sr_only_text.into())
18    }
19}
20
21pub type TableHeadings = Vec<TableHeading>;
22
23#[props]
24pub struct TableProps {
25    headings: TableHeadings,
26    body: Vec<String>,
27}
28
29#[component]
30pub fn Table(props: TableProps) -> String {
31    html! {
32        <table class="min-w-full divide-y divide-gray-300">
33            <TableHeadingsRow headings=props.headings />
34            <TableBody body=props.body />
35        </table>
36    }
37}
38
39pub enum TDVariant {
40    Default,
41    First,
42    Last,
43    LastNonEmptyHeading,
44}
45
46#[props]
47pub struct TableDataProps {
48    children: String,
49
50    #[builder(default=TDVariant::Default)]
51    variant: TDVariant,
52}
53
54#[component]
55pub fn TableData(props: TableDataProps) -> String {
56    let td_class = match props.variant {
57        TDVariant::Default => "whitespace-nowrap px-3 py-4 text-sm text-gray-500",
58        TDVariant::First => {
59            "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6"
60        }
61        TDVariant::Last => {
62            "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
63        }
64        TDVariant::LastNonEmptyHeading => {
65            "whitespace-nowrap py-4 pl-3 pr-4 text-left text-sm font-medium sm:pr-6"
66        }
67    };
68
69    html! {
70        <td class=td_class>{props.children}</td>
71    }
72}
73
74#[component]
75fn TableHeadingsRow(headings: TableHeadings) -> String {
76    html! {
77        <thead class="bg-gray-50">
78            <tr>
79            {headings.iter().enumerate().map(|(i, heading)| {
80                let th_class = match i {
81                    // first heading:
82                    0 => "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6",
83
84                    // last heading:
85                    _ if i == headings.len() - 1 => "py-3.5 pl-3 pr-4 text-left text-sm font-semibold text-gray-900 sm:pr-6",
86
87                    // middle headings:
88                    _ => "px-3 py-3.5 text-left text-sm font-semibold text-gray-900",
89                };
90
91                match heading {
92                    TableHeading::Title(heading) => html! {
93                        <th scope="col" class=th_class>{heading}</th>
94                    },
95                    TableHeading::Empty(sr_only_text) => html! {
96                        <th scope="col" class=format!("relative {}", th_class)>
97                            <span class="sr-only">{sr_only_text}</span>
98                        </th>
99                    },
100                }
101            }).collect_fragment()}
102            </tr>
103        </thead>
104    }
105}
106
107#[component]
108fn TableBody(body: Vec<String>) -> String {
109    html! {
110        <tbody class="divide-y divide-gray-200 bg-white">
111            {
112                body.iter().map(|row| html! {
113                    <tr data-loading-states>{row}</tr>
114                })
115                .collect_fragment()
116            }
117        </tbody>
118    }
119}
120
121#[html_element]
122pub struct ActionLinkProps {
123    children: String,
124    #[builder(setter(into))]
125    sr_text: String,
126}
127
128#[component]
129pub fn ActionLink(props: ActionLinkProps) -> String {
130    html! {
131        <HtmlElement
132            tag="a"
133            class=format!("cursor-pointer text-indigo-600 hover:text-indigo-900, {}", props.class).trim()
134            attrs=spread_attrs!(props | omit(class))
135        >
136            {props.children}<span class="sr-only">{props.sr_text}</span>
137        </HtmlElement>
138    }
139}
140
141#[derive(Clone)]
142pub struct Confirm {
143    pub title: String,
144    pub message: String,
145}
146
147#[html_element]
148pub struct DeleteActionLinkProps {
149    children: String,
150    confirm: Confirm,
151
152    #[builder(setter(into))]
153    sr_text: String,
154
155    #[builder(default = false)]
156    show_loader_on_delete: bool,
157}
158
159#[component]
160pub fn DeleteActionLink(props: DeleteActionLinkProps) -> String {
161    html! {
162        <ActionLink
163            sr_text=props.sr_text
164            attrs=spread_attrs!(props)
165                .set("hx-confirm", props.confirm.title)
166                .set("data-confirm-message", props.confirm.message)
167                .set_if("data-loading-disable", "true".into(), props.show_loader_on_delete)
168        >
169            {if props.show_loader_on_delete {
170                html! {
171                    <div class="htmx-indicator inline-flex animate-spin mr-2 items-center justify-center rounded-full w-4 h-4 bg-gradient-to-tr from-gray-500 to-white">
172                        <span class="inline h-3 w-3 rounded-full bg-white hover:bg-gray-50"></span>
173                    </div>
174                }
175            } else { String::from("") }}
176            {props.children}
177        </ActionLink>
178    }
179}
180
181#[component]
182pub fn TableDataActions(children: String) -> String {
183    html! {
184        <div class="inline-flex gap-4">
185            {children}
186        </div>
187    }
188}