patternfly_yew/components/table/
mod.rs1mod 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#[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 #[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 #[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 #[prop_or_default]
63 pub row_selected: Option<Callback<<M as TableModel<C>>::Item, bool>>,
64
65 #[prop_or_default]
67 pub ouia_id: Option<String>,
68 #[prop_or(OUIA.component_type())]
70 pub ouia_type: OuiaComponentType,
71 #[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#[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 if expandable_columns.is_empty() {
341 <TableData
342 expandable={ExpandParams {
343 r#type: ExpandType::Row,
344 expanded,
345 ontoggle: onclick,
346 }}
347 />
348 }
349 { render_row(props, &entry, |column| expandable_columns.contains(column)) }
351 </TableRow>
352 <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 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}