Customizing tables
This tutorial will give an overview of the possible ways to customize the functionality of the table components, and what to keep in mind with each.
Custom DisplayComponent or EditComponent
Creating a custom DisplayComponent
or EditComponent
is the best way to override whatever is displayed in each row.
It is not really different from creating a custom field, but the former might be preferred if the component in question is only going to be used in a single table on a single page.
Example: Wrapping a generated field
In this example we wrap a generated field for the type Address
that performs some zip-code lookup to automatically fill some of the information
The Address
type in eidos.ts
might look something like:
interface Address extends BaseObject {
name: string;
country: string;
city: string;
street: string;
houseNumber: string;
zipCode: string;
}
This generates a straight-forward AddressField
which allows seperate editing of each property. We create a wrapped version of this field as follows:
import { useCallback } from "react";
import { AddressField } from "../fields/fields.js";
import { getAddress } from "some-geo-library";
import { FieldComponentProps } from "@bronscode/eidos";
import { PartialRec } from "@bronscode/eidos/dist/eidos_std";
function AddressEditComponent({ value, onChange, editable }: FieldComponentProps<Address>) {
const handleChange = (v: PartialRec<Address>) => {
if ((v.houseNumber && value.zipCode) || (v.zipCode && value.houseNumber)) {
const { city, street } = getAddress(zipCode, houseNumber);
onChange({ ...v, city, street });
} else {
onChange(v);
}
};
return <AddressField value={value} onChange={handleChange} editable={editable} />;
}
This way, we retain the automatically generated contents of AddressField
, but we can still adjust the functionality.
Then, it can be used in the normal way inside the EditableTable
.
Row overrides
If neither adjusting the stylesheet nor overriding the Display or Edit component works for your table use case, it might be time to create a custom row override. This is not a recommended way to adjust the table functionality, since the built-in row components handle a lot of functionality such as actions, selection, inline editing and drag reordering. Thus when creating a custom row component, each of these functionalities needs to be added by hand, either by importing the related internal components from eidos, or by reimplementing them. Nevertheless, this section will show how to do so.
RowComponent type signature
The DisplayTable passes the following properties to the RowComponent
:
interface RowProps<T extends BaseObject> {
id: integer;
/** Value to display in row */
value: T;
/** Whether the current row is selected */
isSelected: boolean;
/** Toggle selection of this row */
onSelect: (value: T) => void;
/** The actions record containing named actions to appear next to row*/
actions: Record<string, RowAction<T>>;
/** Object that specifies the subset of the value's keys to be displayed. */
columnLabels?: Record<keyof T, string>;
/** Display Component to render the elements of the row */
DisplayComponent: FC<DisplayComponentProps<T>>;
}
In addition, you can specify a RowComponentProps
object in the arguments of the DisplayTable, and this value will be passed on to the RowComponent
as well.
Note that the RowComponentProps
are shallowly compared between each rerender, so if your RowComponentProps
object in turn contains objects whose values change, the rows will not be updated with these changes.
If you are creating a custom RowComponent
for a single table, you can in principle ignore the columnLabels
and DisplayComponent
properties in your component.
The EditableTable in turn passes the following properties to its RowComponent
:
interface EditWrapperProps<T extends BaseObject> {
id: integer;
/** Value to display in row */
value: T;
/** The actions record containing named actions to appear next to row*/
actions: Record<string, RowAction<T>>;
/** Object that specifies the subset of the row' keys to be displayed.
* If not specified, all keys are displayed */
columnLabels?: Record<keyof T, string>;
/** Edit fields */
EditComponent: FC<EditComponentProps<T>>;
/** Setter function for values in the list */
setObject: (id: number, obj: Partial<T>) => void;
/** Toggle user editing on or off (e.g. based on permissions). Default true. */
editable: boolean;
/** Whether the current row is selected */
isSelected: boolean;
/** Toggle selection of this row */
onSelect: (value: T) => void;
}
As before the columnLabels
and EditComponent
can likely be ignored for a custom RowComponent
.
Finally the OrderedTable extends this by adding reordering:
interface DragWrapperProps<T extends OrderedListMember> extends EditWrapperProps<T> {
/** If draggable, the callback when rows get dragged */
onReorder: (source: number, target: number, location: "above" | "below") => void;
/** Inline editing (can be ignored for overrides) */
inline: boolean;
}
Structure of a RowComponent
The RowComponent
should follow the following structure:
function MyRowComponent({ id, value, isSelected, onSelect, actions }: RowProps<some_type>) {
return (
<div className="TableRow MyRowComponent">
<div>{/* e.g. selection checkbox */}</div>
<div>{/* display the value */}</div>
<div className="RowActions">{/* render the actions */}</div>
</div>
);
}
The div
structure shown here is necessary: the outer div will be the container for the entire row, and the inner div
s will get display: contents
. This brings everything you render inside to the same DOM level, while maintaining seperation between the selection, value, row actions and so on.
Adding row actions and selection
In most cases, you can just import the default RowActions
component from eidos and place this in your RowComponent
instead of rendering the actions yourself. This component also handles the situation where there are more than 3 actions to display in a single row, by moving overflowing actions to a "more actions" button.
So the above structure becomes:
import { RowProps, RowActions } from "@bronscode/eidos";
function MyRowComponent({ id, value, isSelected, onSelect, actions }: RowProps<some_type>) {
return (
<div className="TableRow MyRowComponent">
<div>{/* e.g. selection checkbox */}</div>
<div>{/* display the value */}</div>
<RowActions actions={actions} value={value} />
</div>
);
}
Similarly you can import the RowSelect
component from eidos and render this in place of the first div
.
If you want to render the actions yourself, you will need to know the following type definition:
type RowAction<T extends BaseObject> = {
/** Text label*/
label: string;
/** Icon */
icon: MUIIcon;
/** onClick callback */
onClick: (value: T) => void;
/** Whether this action is disabled */
disabled?: (value: T) => boolean;
/** Variant to display, "button" will display label text, default should be "icon" */
variant?: "icon" | "button";
};
The MoreActions
component is also exported, to which you can pass any actions you wish to be rendered as a dropdown menu.
Header overrides
It is again not recommended to override the HeaderComponent
, since the default implementation already handles the sorting callbacks on each column label.
HeaderComponent type signature
The DisplayTable passes the following properties to the HeaderComponent
:
interface HeaderProps<T extends BaseObject> {
/** First value underneath the header, can be used to get column labels in case columnLabels isn't provided */
firstValue: T;
/** Labels of the columns */
columnLabels?: Partial<Record<keyof T, string>>;
/** The actions record containing named actions to appear next to the column labels */
actions: Record<string, TableAction>;
/** Sort value */
sortValue: any;
/** Sort direction */
sortDirection: SortDirection;
/** Set sort value */
setSortValue: (sort: string) => void;
/** Set sort direction */
setSortDirection: (dir: SortDirection) => void;
/** Whether the entire group or table is completely selected */
isSelected: boolean;
/** Toggle selection of entire group or table */
onSelect: (key: string) => void;
/** Key of the current group */
groupKey: string;
}
This definition also remains the same in the other two tables. As before, you can pass a HeaderComponentProps
object to the DisplayTable
, and this will also be passed along to the HeaderComponent
.
The structure will be similar to that of the RowComponent
, namely:
function MyRowComponent({ id, value, isSelected, onSelect, actions }: RowProps<some_type>) {
return (
<div className="TableRow Header MyHeaderComponent">
<div>{/* e.g. selection checkbox */}</div>
<div>{/* display the column labels */}</div>
<div className="HeaderActions">{/* render the actions */}</div>
</div>
);
}
Adding header actions and selection
As before, you can import the HeaderActions
component to render the header actions. A header action is modelled by a TableAction
, which is slightly simpler than a RowAction
, becauase the current row's value is not needed as argument. The type definition is as follows:
type TableAction = {
label: string;
icon: MUIIcon;
onClick: () => void;
disabled?: boolean;
/** Variant to display, "button" will display label text */
variant?: "icon" | "button";
};
The RowSelect
component can be reused here, since the onSelect
callback passed to the HeaderComponent
will, when called, select the entire table or group that this HeaderComponent
heads.
Adding sorting
The HeaderComponent
also gets setSortValue
and setSortDirection
callbacks.
The sortValue
is to be the key of that column, and the sortDirection
is either "asc"
or "desc"
.
If necessary the default comparison can also be overridden by specifying a custom sortFunc
in the DisplayTable
's properties. This entails implementing a comparison for each column key and each sort direction.
Toolbar overrides
Lastly, it is also possible to override the ToolbarComponent
. This is the most straightforward, but is also the least likely to be necessary. The ToolbarComponent
gets the following properties:
interface ToolbarProps {
/** Title of toolbar */
title: string;
/** The actions record containing named actions to appear right of search bar */
actions: Record<string, TableAction>;
/** Value to serch */
search: any;
/** Callback on search change */
setSearch: (search: any) => void;
}
Your ToolbarComponent
's structure is not as important. It might look something like:
function MyToolbar({ title, actions, search, setSearch }: ToolbarProps) {
return (
<div className="TableToolbar">
{/* render title */}
{/* render search */}
<div className="ToolbarActions">{/* render actions */}</div>
</div>
);
}
Again, eidos exports a ToolbarActions
component, which will also handle displaying each action as an icon or as a button depending on the variant specified.