Skip to main content

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 divs 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.