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!(
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 if expandable_columns.is_empty() {
343 <TableData expandable={ExpandParams {
344 r#type: ExpandType::Row,
345 expanded,
346 ontoggle: onclick,
347 }} />
348 }
349 { render_row(props, &entry, |column| expandable_columns.contains(column)) }
351 </TableRow>
352
353 <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 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}