Skip to main content

patternfly_yew/components/table/
mod.rs

1mod cell;
2mod column;
3mod composable;
4mod header;
5mod model;
6mod props;
7mod render;
8
9pub use cell::*;
10pub use column::*;
11pub use composable::*;
12pub use header::*;
13pub use model::*;
14pub use props::*;
15pub use render::*;
16
17use crate::ouia;
18use crate::prelude::{Dropdown, ExtendClasses, Icon, MenuChildVariant, MenuToggleVariant};
19use crate::utils::{Ouia, OuiaComponentType, OuiaSafe};
20use yew::{prelude::*, virtual_dom::VChild};
21
22const OUIA: Ouia = ouia!("Table");
23
24/// Properties for [`Table`]
25#[derive(PartialEq, Clone, Properties)]
26pub struct TableProperties<C, M>
27where
28    C: Clone + Eq + 'static,
29    M: PartialEq + TableModel<C> + 'static,
30{
31    #[prop_or_default]
32    pub class: Classes,
33
34    #[prop_or_default]
35    pub id: AttrValue,
36
37    #[prop_or_default]
38    pub caption: Option<String>,
39    #[prop_or_default]
40    pub mode: TableMode,
41    /// Borders or borderless.
42    ///
43    /// Defaults to borders being enabled.
44    #[prop_or(true)]
45    pub borders: bool,
46    #[prop_or_default]
47    pub header: Option<VChild<TableHeader<C>>>,
48    #[prop_or_default]
49    pub full_width_details: bool,
50    pub entries: M,
51
52    /// When to switch to grid mode
53    #[prop_or_default]
54    pub grid: Option<TableGridMode>,
55
56    #[prop_or_default]
57    pub onexpand: OnToggleCallback<C, M>,
58
59    #[prop_or_default]
60    pub onrowclick: Option<Callback<<M as TableModel<C>>::Item>>,
61    /// Callback stating whether a given row is selected or not.
62    #[prop_or_default]
63    pub row_selected: Option<Callback<<M as TableModel<C>>::Item, bool>>,
64
65    /// OUIA Component id
66    #[prop_or_default]
67    pub ouia_id: Option<String>,
68    /// OUIA Component Type
69    #[prop_or(OUIA.component_type())]
70    pub ouia_type: OuiaComponentType,
71    /// OUIA Component Safe
72    #[prop_or(OuiaSafe::TRUE)]
73    pub ouia_safe: OuiaSafe,
74}
75
76impl<C, M> TableProperties<C, M>
77where
78    C: Clone + Eq + 'static,
79    M: PartialEq + TableModel<C> + 'static,
80{
81    pub fn is_expandable(&self) -> bool {
82        matches!(
83            self.mode,
84            TableMode::Expandable | TableMode::CompactExpandable
85        )
86    }
87
88    pub fn are_columns_expandable(&self) -> bool {
89        if let Some(header) = &self.header {
90            header
91                .props
92                .children
93                .iter()
94                .any(|table_column| table_column.props.expandable)
95        } else {
96            false
97        }
98    }
99}
100
101/// Table component
102///
103/// > A **table** is used to display large data sets that can be easily laid out in a simple grid with column headers.
104///
105/// See: <https://www.patternfly.org/components/table/html>
106///
107/// ## Properties
108///
109/// Defined by [`TableProperties`].
110///
111/// ## Usage
112///
113/// The table component is a more complex component than most others. It is recommended to check
114/// out the more complete examples in the quickstart project: <https://github.com/patternfly-yew/patternfly-yew-quickstart/tree/main/src/components/table>.
115///
116/// Summarizing it, you will need:
117///
118/// * A type defining the column/index (this can be an enum or a numeric like `usize`).
119/// * A type defining an item/entry/row.
120/// * Let the item type implement [`TableEntryRenderer`].
121/// * Create a table state model (e.g. using [`MemoizedTableModel`]).
122/// * Wire up the table state model (e.g. using [`use_table_data`]).
123///
124/// If the table is too limiting (one example is wanting to save state per row) you may have to
125/// use [`ComposableTable`].
126/// However, it is recommended to use [`Table`] where possible.
127///
128/// ## Example
129///
130/// ```rust
131/// use yew::prelude::*;
132/// use patternfly_yew::prelude::*;
133///
134/// #[derive(Copy, Clone, Eq, PartialEq)]
135/// enum Column { First, Second };
136/// #[derive(Clone)]
137/// struct ExampleEntry { foo: String };
138///
139/// impl TableEntryRenderer<Column> for ExampleEntry {
140///   fn render_cell(&self, context: CellContext<'_, Column>) -> Cell {
141///     match context.column {
142///       Column::First => html!(&self.foo).into(),
143///       Column::Second => html!({self.foo.len()}).into(),
144///     }
145///   }
146/// }
147///
148/// #[function_component(Example)]
149/// fn example() -> Html {
150///
151///   let entries = use_memo((), |()| {
152///       vec![
153///           ExampleEntry { foo: "bar".into() },
154///           ExampleEntry {
155///               foo: "Much, much longer foo".into(),
156///           },
157///       ]
158///   });
159///
160///   let (entries, _) = use_table_data(MemoizedTableModel::new(entries));
161///
162///   let header = html_nested! {
163///     <TableHeader<Column>>
164///       <TableColumn<Column> label="foo" index={Column::First} />
165///       <TableColumn<Column> label="bar" index={Column::Second} />
166///     </TableHeader<Column>>
167///  };
168///
169///   html! (
170///     <Table<Column, UseTableData<Column, MemoizedTableModel<ExampleEntry>>>
171///       {header}
172///       {entries}
173///     />
174///   )
175/// }
176/// ```
177#[function_component(Table)]
178pub fn table<C, M>(props: &TableProperties<C, M>) -> Html
179where
180    C: Clone + Eq + 'static,
181    M: PartialEq + TableModel<C> + 'static,
182{
183    let expandable_columns = use_memo(
184        (props.header.clone(), props.mode.is_expandable()),
185        |(header, expandable)| {
186            if !expandable {
187                return vec![];
188            }
189
190            match header {
191                Some(header) => header
192                    .props
193                    .children
194                    .iter()
195                    .filter_map(|c| c.props.expandable.then(|| c.props.index.clone()))
196                    .collect::<Vec<_>>(),
197                None => vec![],
198            }
199        },
200    );
201
202    let expandable = props.is_expandable() && !props.are_columns_expandable();
203    html!(
204        <ComposableTable
205            id={&props.id}
206            class={props.class.clone()}
207            sticky_header={props.header.as_ref().is_some_and(|header| header.props.sticky)}
208            mode={props.mode}
209            borders={props.borders}
210            grid={props.grid}
211            ouia_id={props.ouia_id.clone()}
212            ouia_type={props.ouia_type}
213            ouia_safe={props.ouia_safe}
214        >
215            if let Some(caption) = &props.caption {
216                <Caption>{ caption }</Caption>
217            }
218            if let Some(header) = props.header.clone() {
219                <TableHeader<C> {expandable} ..(*header.props).clone() />
220            }
221            { render_entries(props, &expandable_columns) }
222        </ComposableTable>
223    )
224}
225
226fn render_entries<C, M>(props: &TableProperties<C, M>, expandable_columns: &[C]) -> Html
227where
228    C: Clone + Eq + 'static,
229    M: PartialEq + TableModel<C> + 'static,
230{
231    if props.is_expandable() {
232        props
233            .entries
234            .iter()
235            .map(|entry| render_expandable_entry(props, entry, expandable_columns))
236            .collect()
237    } else {
238        let row_click_cb = {
239            let onrowclick = props.onrowclick.clone();
240            Callback::from(move |entry| {
241                if let Some(f) = onrowclick.as_ref() {
242                    f.emit(entry)
243                }
244            })
245        };
246        html!(
247            <TableBody>
248                { for props.entries.iter().map(|entry| {
249                    let selected = props.row_selected.as_ref().is_some_and(|f| f.emit(entry.value.clone()));
250                    let content = { render_row(props, &entry, |_| false)};
251                    let onclick = if props.onrowclick.is_some() {
252                        let cb = row_click_cb.clone();
253                        let val: M::Item = entry.value.clone();
254                        Some(Callback::from(move |_| cb.emit(val.clone())))
255                    } else {
256                        None
257                    };
258                    html! {
259                        <TableRow {onclick} {selected}>
260                            {content}
261                        </TableRow>
262                    }
263                }) }
264            </TableBody>
265        )
266    }
267}
268
269fn render_expandable_entry<C, M>(
270    props: &TableProperties<C, M>,
271    entry: TableModelEntry<M::Item, M::Key, C>,
272    expandable_columns: &[C],
273) -> Html
274where
275    C: Clone + Eq + 'static,
276    M: PartialEq + TableModel<C> + 'static,
277{
278    let expansion = entry.expansion.clone();
279    let expanded = expansion.is_some();
280
281    let key = entry.key.clone();
282
283    let mut cols = props
284        .header
285        .as_ref()
286        .map_or(0, |header| header.props.children.len())
287        + 1;
288
289    let mut cells: Vec<Html> = Vec::with_capacity(cols);
290
291    if expandable_columns.is_empty()
292        && !entry
293            .value
294            .is_full_width_details()
295            .unwrap_or(props.full_width_details)
296    {
297        cells.push(html! { <TableData /> });
298        cols -= 1;
299    }
300
301    let details = match expansion {
302        Some(ExpansionState::Row) => entry.value.render_details(),
303        Some(ExpansionState::Column(col)) => entry.value.render_column_details(&col),
304        None => vec![],
305    };
306
307    for cell in details {
308        cells.push(html! {
309            <TableData span_modifiers={cell.modifiers.clone()} colspan={cell.cols}>
310                <ExpandableRowContent>{ cell.content }</ExpandableRowContent>
311            </TableData>
312        });
313
314        if cols > cell.cols {
315            cols -= cell.cols;
316        } else {
317            cols = 0;
318        }
319        if cols == 0 {
320            break;
321        }
322    }
323
324    if cols > 0 {
325        cells.push(html!(<TableData colspan={cols} />));
326    }
327
328    let onclick = {
329        let key = key.clone();
330        props
331            .onexpand
332            .0
333            .reform(move |_| (key.clone(), ExpansionState::Row))
334    };
335
336    html!(
337        <TableBody {key} {expanded}>
338            <TableRow control_row={!expandable_columns.is_empty() && props.mode.is_expandable()}>
339                // first column, the toggle
340                if expandable_columns.is_empty() {
341                    <TableData
342                        expandable={ExpandParams {
343                        r#type: ExpandType::Row,
344                        expanded,
345                        ontoggle: onclick,
346                    }}
347                    />
348                }
349                // then, the actual content
350                { render_row(props, &entry, |column| expandable_columns.contains(column)) }
351            </TableRow>
352            // the expanded row details
353            <TableRow expandable=true {expanded}>{ cells }</TableRow>
354        </TableBody>
355    )
356}
357
358fn render_row<C, M, F>(
359    props: &TableProperties<C, M>,
360    entry: &TableModelEntry<'_, M::Item, M::Key, C>,
361    expandable: F,
362) -> Html
363where
364    C: Clone + Eq + 'static,
365    M: PartialEq + TableModel<C> + 'static,
366    F: Fn(&C) -> bool,
367{
368    let actions = entry.value.actions();
369
370    let cols = props
371        .header
372        .iter()
373        .flat_map(|header| header.props.children.iter());
374
375    html!(
376        <>
377            { for cols.map(|column| {
378
379            let index = column.props.index.clone();
380            let expandable = expandable(&index);
381
382            // main cell content
383            let cell = entry.value.render_cell(CellContext {
384                column: &column.props.index,
385            });
386
387            let key = entry.key.clone();
388            let expandable = expandable.then(|| ExpandParams {
389                r#type: ExpandType::Column,
390                ontoggle: props.onexpand.0.reform({
391                    let index = index.clone();
392                    move |_| {
393                        let toggle = ExpansionState::Column(index.clone());
394                        (key.clone(), toggle)
395                    }
396                }),
397                expanded: entry.expansion == Some(ExpansionState::Column(index.clone())),
398            });
399
400            html!(
401                <TableData
402                    data_label={column.props.label.clone().map(AttrValue::from)}
403                    {expandable}
404                    center={cell.center}
405                    text_modifier={cell.text_modifier}
406                >
407                    { cell.content.clone() }
408                </TableData>
409            )
410        }) }
411            <RowActions {actions} />
412        </>
413    )
414}
415
416#[derive(PartialEq, Properties)]
417struct RowActionsProperties {
418    actions: Vec<MenuChildVariant>,
419}
420
421#[function_component(RowActions)]
422fn row_actions(props: &RowActionsProperties) -> Html {
423    html!(
424        <>
425            if !props.actions.is_empty() {
426                <TableData action=true>
427                    <Dropdown variant={MenuToggleVariant::Plain} icon={Icon::EllipsisV}>
428                        { props.actions.clone() }
429                    </Dropdown>
430                </TableData>
431            }
432        </>
433    )
434}
435
436#[derive(Clone, Debug, PartialEq)]
437pub struct Span {
438    pub cols: usize,
439    pub content: Html,
440    pub modifiers: Vec<SpanModifiers>,
441}
442
443impl Span {
444    pub fn one(html: Html) -> Self {
445        Self {
446            cols: 1,
447            content: html,
448            modifiers: Vec::new(),
449        }
450    }
451
452    pub fn max(html: Html) -> Self {
453        Self {
454            cols: usize::MAX,
455            content: html,
456            modifiers: Vec::new(),
457        }
458    }
459
460    pub fn truncate(mut self) -> Self {
461        self.modifiers.push(SpanModifiers::Truncate);
462        self
463    }
464}