DataTable
A structure that organizes and displays data in sortable grid.
The Basics
What it is
DataTable structures and presents related data points in a grid layout for quick comparison and analysis.
How it works
-
DataTable can be set to client-side fetching, where data is loaded in memory based on the rowData prop. Alternatively, server-side fetching can be used to load the data progressively for larger data sets.
-
Hovering on a row highlights it with a blue background color, to help the user scan across the table.
-
If the DataTable has sortable columns, those column headers have arrow icons for sorting. The user can click the sorting icons (or focus and press the Enter key or Space bar) to reorder the column data in ascending or descending order.
-
If any rows are expandable, the user can click the row’s expand control to show/hide an additional container below it (following rows are pushed down). Sub-rows are not automatically populated and require additional code.
When to use
- To organize and display structured information
- To help users find and compare data points
When not to use
For simple image- or text-based lists that do not need as much structure
What to use instead
Use List if you are trying to display a less structured list of data
How to use
DataTable has 3 core elements:
- Column headers are the descriptive titles that appear at the top of each column. Display your columns from left to right in a meaningful order, such as by importance or alphabetically. The first column should contain a unique identifier for each row (e.g., "Name" or "ID" in a table of patients) so users can scan data easily, even after it's been sorted or filtered.
- Colum headers can be optionally made sticky using the “stickyHeaders” prop
- Data columns are the table's vertical sections. Each column displays data that corresponds to its column header. By default, text is left-aligned, and numbers are right-aligned for easier comparison.
- Data rows are the horizontal sections that span the data columns. Each row contains one unique set of data points.
Options – Layout and display
- Layout: DataTable offers 2 layout settings:
- Medium (default) is suitable for most tables.
- Compact is best for dense data sets with a lot of numbers, like a table of recent transactions.
- Loader: Uses the Loader component. We recommend enabling the loader by default so that there is an indication of loading in the longer load times.
- Column sorting: Sorting can be enabled for individual columns. This lets users change the order of values in that column (e.g., sorting payment amounts from smallest to largest). This action reorders all rows in the Table. The default sorting order is ascending and descending. You can also build custom handling for more complex data points (ex., height, split into feet and inches).
- Row hover state: DataTable includes a row hover state (turned on by default). This highlights an entire row when users hover over any part of it.
- Expandable Row: Your table design might need to make additional details available in the context of the table but not display them by default. We recommend putting these details in an expandable detail row directly below the visible “parent” data row they correspond to.
- Detail rows are hidden/collapsed by default.
- When row detail information is passed in, the parent row will automatically include a clickable arrow for revealing the detail row.
- You can insert other components (ex., cards) or custom HTML. If you are considering inserting another table in the expanded area, evaluate whether that data should be included in the parent table.
Options – Pagination
This component supports pagination (i.e., chunking the table contents into multiple pages that can be navigated). The Paginator component is built in to allow navigation. We recommend turning on pagination if you have more than 20 rows in your table.
Choose one of these pagination approaches:
- Client-side: Your application provides the full table contents, and the component automatically handles dividing and displaying it in pages. This is managed by splitting the
rowData
prop into smaller chunks. - Server-side: Your application provides only the “page” or chunk of table contents that should be currently visible. This is managed via the
serverSideFetchRowDataFn
callback function.
These supporting elements for pagination are included:
- Paginator: Used for navigating among the pages of content.
- Paginator is displayed at the top right (above the table) by default. It can also be set to appear in bottom right corners, too, to provide quick navigation from the bottom of the page.
- Paginator can be set to either its default or compact mode. Compact is recommended to allow layout space for other table functionality in the same control row (e.g., bulk action, search, etc.).
- Row count: Displays the number of rows displayed and total rows (e.g., “1-20 of 375”).
- This appears next to the Paginator.
- If you do not have an exact count of the number of rows, you can set the row count to show an approximate (imprecise) count. You can set the imprecise text to use the format “A-B of ~X” or “A-B of X+” with units of K and M auto applying (ex 3M+)
- Set rows per page: When row counts are in use, you can allow the user to set how many rows are displayed per page. With this setting enabled, the row count becomes a clickable menu. The user can select how many rows they would like to be able to see at a time. These increments can be set by the consuming application to easily enable teams to set increments that make sense for their dat.
Options – Filtering and sorting
- Filters: Enables filtering on the table data.
- o Filters appear automatically in a panel above the table. The panel can be set to always open or to be collapsible through the "hidable" prop, to enable more space on the page. We have plans to support a simple column filter side filter panel that work is currently in progress.
- If you have many filters or want to sort your filters by topic, we recommend using the
quickFilter
prop. This will allow you to create header sections for your filter panel for grouping the filters more elegantly. - We have made adding filters easier by providing support for all current input types that could be filters (e.g., date input, multiselect, etc.) and ensuring the formatting works as intended. Using the
filters
prop, you can find all the currently supported filters and their built-in logic. - For app-controlled filters that may be located elsewhere on the page or not visible at all, a custom
filter
can be written
- Search: Separate from the filter panel, the table also offers a simple text search. The search input field appears immediately above the table in the control row and provides a string-based text search. Consumers can turn off search on specific columns (ex. if you want to have only one searchable column). Search will bring up results with partial matches. The search functionality can be client-side (which works immediately upon typing) or server-side (which requires the user to enter the string first and click the search button).
- Empty state: Uses the EmptyState component. This option lets you show a placeholder message when the table has no data to display (e.g., the filters don’t match to any data, the search function returned no results, the user has completed all available tasks, etc.).
Options – DataTable content editing and actions
- Action buttons: Action buttons are located in the right-most cell of each row. This provides a standard, grouped location for any row-level actions you want to provide.
- Button format – The array of action buttons defaults to icon-only buttons, but other button types can be used. They will shrink for compact table layouts.
- Edit & Delete – We provide standard the Edit (pencil) and Delete (trash) action as part of the table. These will enable row edit (see edit below) and delete for a row (see deleted rows below).
- Custom actions – We have also enabled consumers to add their own actions to the action column. Teams will have to provide their own functionality behind other actions.
- Empty data: If a cell contains empty, or null data, it is marked with dashes (---). However, different types of columns represent null data differently. String-based columns, like TextColumn or RadioGroupColumn represent their empty data with the empty string (
''
). If a column is number or object based, likeNumericColuimn
orDateInputColumn
, thennull
is used instead. - Deleted/ Archived/ Voided rows: We have standardized the look and interaction on deleted, archived, and voided rows.
- When a row is marked as deleted, its row and font color change, its text is italicized, and its actions are automatically disabled except for an action to ‘Restore’ the row.
- If a row is restored, its style will be returned to normal (row color, font color, text style) and its actions will be re-enabled.
- Note: If you are using custom cell renders you will need to supply a deleted version in your cell renderer to be able to use the feature.
- Add entity: Users can add a row to your table through the UI with either a lightbox or a form panel above the table. When a user adds the row, we also refresh the data table and data can be passed in in any given order as determined by the consuming team.
- Bulk action: We have built in the ability to act on multiple rows at the same time. Please be mindful of the language you are putting into your bulk action sections to be clear about the exact action that the user is taking. For example, instead of “Bulk Edit” or “Bulk Assign”, you can put “Update Status” or “Assign Claims”.
- Selecting rows for bulk action: Each row includes a checkbox on the far-left side. When selected, a row will automatically appear shaded for easy scanning. The “select all” checkbox in the table’s left-most column header will select all rows on the current visible page only. If you select a row and then navigate to another page, the row selections on the initial page persist. When the bulk action is taken, all selected rows across all the pages are sent to the consuming application. The consuming action will always need to program the result of the bulk action.
- Single simple bulk action - Button: If you have a simple bulk action where you only ever need to provide one action for the user to take (ex., Assign), we offer a single bulk action button on the left side of the control row. The button text will show the number of rows selected, but the application will need to provide the text to describe the bulk action itself.
- Multiple simple bulk actions - Menu: If you have a small number of actions that don’t need additional detail to describe, we provide a bulk action dropdown menu on the left side of the control row. The number of rows selected will appear in the menu text, but the application will need to provide the menu option text for the bulk actions. The menu can include actions that require a lightbox or modal, but those should be grouped in a section of the menu, separate from quick actions.
- Medium complexity bulk actions - Control row: If you need more space to display a simple bulk action that doesn’t require a modal or lightbox, we provide the option for bulk action in a second control row, situated between the standard control row and the column headers. The bulk action row indicates the number of rows selected, followed by a one-line form element. This setup provides slightly more space to give context for what the action is by, for example, writing it out as a sentence (ex., 25 Plans Selected / Change status to [selector with list of statuses] and reassign to [selector with list of users]). This format should be used if you need to provide additional context on the bulk action and can keep it to a single line.
- Complex bulk actions - Modal: If you have a more complicated bulk action and you need to provide the user with additional details (or kick off a workflow), this component provides a button that launches a Modal intended for bulk actions. The information inside the modal is completely application controlled.
- Inline edit: We currently offer two types of inline edit built into the table, both of which provide built-in edit fields for selects and inputs (including date input). For custom cells, if you want it to be editable in the table itself you will need to provide an editable state for the cell. We also enable the application to determine which columns and rows are editable with the ability to disable editing at a per cell level. For bulk edit (i.e., making the same change to multiple rows at one time) please see Bulk Action above. Note: You can only have one type of edit available on the table at once. We are planning on adding single cell inline edit functionality, too, which will enable editing/saving one cell at a time. That is currently in development. You can only have one type of edit available on the table at once.
- Table Edit – We provide a table level edit that is enabled above the table. When toggled, all fields that have been set to have an editable state will switch to the input version. We will not send the data to the client until user hits save. Save is done on the entire table.
- Row Edit – We provide an action button of edit (pencil) that can be included for row level editing. When enabled all cells in the row clicked will become editable and the icons in the action field will transition to a save and cancel button (all other actions are restricted when a row is in edit). Data will be sent to the consuming application when the user hits save.
- Validation – Consumers can provide validation that they want to appear in the edit views before the use clicks save. The logic for validation is left to the consumers but will appear below the editing field when it is missing. The validation can prevent the user from saving and follow the same guidance as [Standard Error Banner]
- Alerting when navigating away – To prevent users from losing unsaved changes if the user tries to navigate away or close the tab while an edit has not been executed, we will automatically generate a pop-up asking them if they would like to change noting the unsaved changes.
Options – Other
- CSV Download: You can enable downloading a copy of the data available in the table into a CSV.
- To allow downloading, enable the csvDownload prop.
- If you want to pass in data that is not visible in the table or want to enabledownloads that filter based on filter criteria (see filtering below) or search (see search below), use fetchCSV which will expect the return of the CSV you want to generate.
- Refresh: DataTable data is automatically fetched on page reload, but if you have a need to allow your users to refresh data more frequently, we offer the ability to manually refresh the data. When enabled the refresh icon will appear above the table with the last time the data was pulled. When the button is clicked the table will call the consuming application to fetch the data again. The last fetch time of data can be displayed next to the refresh button allowing the user to easily see when it was last updated.
Large Data Sets
DataTables with large data sets (500+ items) can hinder page performance and usability. If your use case features a large data set, you should:
- Enable filters and search functionality to help users reduce the data in view (usually with Multiselect, SingleSelect, RadioButton, or CheckboxButton)
- Paginate the DataTable content, breaking it into manageable chunks that users can navigate with Paginator
- Use server-side pagination of the table to improve response times.
- When using server-side pagination it is on the consuming team to provide the appropriate end points for pagination and filtering to hook into the table component.
- Enable loader so that there is indication of progress for longer load times.
Style
Design details
DataTable’s column widths are typically determined by the longest string in each column, which makes columns widths vary. This makes it easier for users to scan the table, because it reduces extra white space in the table's cells and shows data points closer together.
By default, DataTable’s width is the sum of its column widths. DataTable width can be set to a specific size, but it shouldn’t be too wide, because this can add white space between data points that makes it harder to scan the table. For this reason, don’t set small DataTables to span the full width of a page.
Row height is set by the layout option:
- Medium (default): 36px row height
- Compact: 24px row height
Placement and hierarchy
Content
To add a title to the DataTable, use the `title` prop. Use title case for this heading (“Today’s Schedule”, not “Today’s schedule”).
Use title case for column headers (“Date of Service”, not “Date of service”). Column headers are automatically displayed in bold. To avoid text wrapping:
- Keep column header text short. Leave out information that’s obvious from the context or the DataTable title. For example, if the DataTable title is “Patients”, use “Name” as a column header, not “Patient Name”.
- Use DataTable’s built-in option for forcing the column header text onto a single line. With this setting enabled, header text is automatically truncated with an ellipsis when the column width is smaller than the width of the header text.
Demos
Coding
Developer tips
Storybook files
DataTable has so many features that it's difficult to create representative demos for everything in this guide, as performance of this page would degrade. For this reason, it's strongly encouraged to view the storybook files in the source code and see them run at http://go/forge-storybook.
createDataTable factory function
DataTable breaks many paradigms shared by a typical Forge component, in part because it is so huge. There is no DataTable
export. Instead, most aspects of DataTable are accessed via the createDataTable
factory function.
const DT = createDataTable<RowData>({ tableId: 'patient-table' });const patients:RowData[] = [...]const PatientTable = (): ReactElement => {return <DT.DataTable tableType="client-side" columns={columns} rowData={patients} {...props} />}
The reason behind the createDataTable
factory function is to bind the type of RowData
to the implementation. It's important to call createDataTable outside of a component's render function. Otherwise, React will unmount and remount the entire table on every re-render of your component.
While Forge does export all of the Typescript types cited in the props documentation, we also provide abstractions of these types for easier access. For example, to define your own CustomAddRowComponent
, you need the DataTableHooks
to define its supported props. You can either
import { DataTableHooks } from '@athena/forge';const CustomAddRowComponent = (props: DataTableHooks<RowData>): ReactElement => <></>;
or
const DT = createDataTable<RowData>({ tableId: 'patient-table' });const CustomAddRowComponent = (props: typeof DT.Type.Hooks): ReactElement => <></>;
See createDataTable implementation for details of what is included.
DataTable vs. SingletonDataTable vs. Advanced DataTable
- DT.DataTable works like a regular React component. Whenever the props of your DataTable change, the entire DataTable component will be re-rendered. You can use
ref.current.getState
to get the current internal state andref.current.dispatch
to change it. - DT.SingletonDataTable can only be used if your React app needs just one DataTable instance. DT.SingletonDataTable performs better than DT.DataTable because it only re-renders the subcomponents when specific states change. You can also use
ref.current.getState
to get the current internal state andref.current.dispatch
to change it.- You can't create a reusable React component using based on the SingletonDataTable.
- SingletonDataTable is implemented using the React Context API.
- Advanced DataTable: DT.Provider, DT.DataTableRender, DT.DataTableInterface, DT.useSelector, and DT.useDispatch are used for the scalable DataTable component.
- Scalability: Advanced DataTable > DT.SingletonDataTable > DT.DataTable. It’s recommended to start with the DT.DataTable.
Repository
Implementation links
DataTable directory in Bitbucket
Implementation details
It is strongly recommended to familiarize yourself with the Forge source code. While this documentation is a best effort to document the intent and usage of a component, sometimes some features only become clear when looking at the source code. Also, looking at Forge's source code may help identify and fix bugs in either your application or Forge itself.
Storybook files
Forge maintains at least one storybook file per component. While the primary audience for these files is typically the Forge team, these storybook files may cover usages of the component not covered by a demo. The storybook for the latest version of forge can be found at go/forge-storybook.
Testing library
Forge strongly encourages using testing-library to write tests for your application.
"The more your tests resemble the way your software is used, the more confidence they can give you."
If you're having trouble testing a Forge component using testing-library, it would be a good idea to see how Forge tests its own components. For the most part, Forge tries to use screen.getByRole as much as it can, as that API provides the best feedback on a11y compliance. Forge discourages the use of document.querySelector and screen.getByTestId as both APIs encourage using implementation details to test your component, and discourage adding roles to your component.
With that being said, many of Forge's components were not built with accessibility in mind. These components do break the recommendations listed above.
Import statements
In Nimbus applications
athenaOne serves the Forge bundle independently from your application's bundle. Importing Forge components directly from '@athena/forge' takes advantage of this feature.
import { createDataTable } from '@athena/forge'
In standalone applications
Importing components using the exact path to the module takes advantage of webpack's tree shaking feature. Webpack will include only that module and its dependencies.
import { createDataTable } from '@athena/forge/DataTable';
To use this import guidance, Typescript applications must use typescript >= 4.7.3, and should add this setting to their tsconfig.json file:
{ "compilerOptions": { "moduleResolution": "Node16", } } If this setting doesn't work for your application, use this import statement instead:
import { createDataTable } from '@athena/forge/dist/DataTable';