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!(
326            <TableData colspan={cols}/>
327        ));
328    }
329
330    let onclick = {
331        let key = key.clone();
332        props
333            .onexpand
334            .0
335            .reform(move |_| (key.clone(), ExpansionState::Row))
336    };
337
338    html!(
339        <TableBody {key} {expanded}>
340            <TableRow control_row={!expandable_columns.is_empty() && props.mode.is_expandable()}>
341                // first column, the toggle
342                if expandable_columns.is_empty() {
343                    <TableData expandable={ExpandParams {
344                        r#type: ExpandType::Row,
345                        expanded,
346                        ontoggle: onclick,
347                    }} />
348                }
349                // then, the actual content
350                { render_row(props, &entry, |column| expandable_columns.contains(column)) }
351            </TableRow>
352
353            // the expanded row details
354            <TableRow expandable=true {expanded}>
355                { cells }
356            </TableRow>
357        </TableBody>
358    )
359}
360
361fn render_row<C, M, F>(
362    props: &TableProperties<C, M>,
363    entry: &TableModelEntry<'_, M::Item, M::Key, C>,
364    expandable: F,
365) -> Html
366where
367    C: Clone + Eq + 'static,
368    M: PartialEq + TableModel<C> + 'static,
369    F: Fn(&C) -> bool,
370{
371    let actions = entry.value.actions();
372
373    let cols = props
374        .header
375        .iter()
376        .flat_map(|header| header.props.children.iter());
377
378    html!(<>
379        { for cols.map(|column| {
380
381            let index = column.props.index.clone();
382            let expandable = expandable(&index);
383
384            // main cell content
385            let cell = entry.value.render_cell(CellContext {
386                column: &column.props.index,
387            });
388
389            let key = entry.key.clone();
390            let expandable = expandable.then(|| ExpandParams {
391                r#type: ExpandType::Column,
392                ontoggle: props.onexpand.0.reform({
393                    let index = index.clone();
394                    move |_| {
395                        let toggle = ExpansionState::Column(index.clone());
396                        (key.clone(), toggle)
397                    }
398                }),
399                expanded: entry.expansion == Some(ExpansionState::Column(index.clone())),
400            });
401
402            html!(
403                <TableData
404                    data_label={column.props.label.clone().map(AttrValue::from)}
405                    {expandable}
406                    center={cell.center}
407                    text_modifier={cell.text_modifier}
408                >
409                    { cell.content.clone() }
410                </TableData>
411            )
412        })}
413
414        <RowActions {actions} />
415    </>)
416}
417
418#[derive(PartialEq, Properties)]
419struct RowActionsProperties {
420    actions: Vec<MenuChildVariant>,
421}
422
423#[function_component(RowActions)]
424fn row_actions(props: &RowActionsProperties) -> Html {
425    html!(<>
426        if !props.actions.is_empty() {
427            <TableData action=true>
428                <Dropdown
429                    variant={MenuToggleVariant::Plain}
430                    icon={Icon::EllipsisV}
431                >
432                    { props.actions.clone() }
433                </Dropdown>
434            </TableData>
435        }
436    </>)
437}
438
439#[derive(Clone, Debug, PartialEq)]
440pub struct Span {
441    pub cols: usize,
442    pub content: Html,
443    pub modifiers: Vec<SpanModifiers>,
444}
445
446impl Span {
447    pub fn one(html: Html) -> Self {
448        Self {
449            cols: 1,
450            content: html,
451            modifiers: Vec::new(),
452        }
453    }
454
455    pub fn max(html: Html) -> Self {
456        Self {
457            cols: usize::MAX,
458            content: html,
459            modifiers: Vec::new(),
460        }
461    }
462
463    pub fn truncate(mut self) -> Self {
464        self.modifiers.push(SpanModifiers::Truncate);
465        self
466    }
467}