List composition is based on using CSS grid to power the alignment and responsivness of rows. The main components used for composing lists are:
The main wrapper for your list. This component will wrap each row you are rendering. Accepted props:
children
: Mainly your row components. Use ListRow
for this type of list.columns
: Array to describe each column: { heading, width, loader }
.isLoadingRows
: Boolean to idicate row loader should be shown.onScrollToBottom
: Callback function that will be called when list is scrolled to bottom. Used to load more items.listStyle
: Additional style props to pass to the list wrapper.Same as ListView
, used to create selectable lists. Additional props:
selectionMap
: A {[rowKey]: boolean}
mapping that describes which rows are selected.onSelectionChange
: Callback function to be called with an updated map as selection changes so you can update your state.disabled
: Are the selection checkboxes disabled. This will not disable individual row clicks.selectionAlwaysVisible
: Checkboxes are always visible, this is great for mobile screens where you can’t hover to make the checkboxes appear.Use SelectableListBoundary
to enable drag to select: This component defines the area over which you can drag to select. Note that
this component should have a selectable list some where down the component tree.
Each row in the list should be rendered as a ListRow
. Depending on your use case,
you should use this component to wrap 1 or more ListRowCell
as described below.
Props:
onClick
: Callback to call when row is clicked.disabled
: Disables row clicks.testId
Same as ListRow
, used as a row in SelectableListView
. You must provide a rowKey
prop. This key represents the id of the row and
is used when selected items change to update the selection map.
Every row should contain at least one cell. A cell is used to group components, although the grouping largely depends on the use case.
Avatars and Text are a good example of components that can be grouped together.
The number of cells in each row should be the same, and the number of columns provided to colWidths
should match the number of cells in each row.
ListItemAction
is a special cell that contains row actions, and should be considered as any other cell.
Basic usage of the <ListView />
. Each item is rendered as a ListRow
with a single ListRowCell
wrapping the content.
const renderListItem = (item) => (
<ListRow>
<ListRowCell>
<Avatar mr={3} size={36} username={item.owner.username} />
<ListText kind="bold">{item.title}</ListText>
</ListRowCell>
</ListRow>
);
return <ListView>{projectsSetA.map(renderListItem)}</ListView>;
To create a selectable list, you will need to use a SelectableListView
.
Use a SelectableListRow
to render selectable rows and provide rowKey
as a unique id for the row.
To enable drag to select, you can use SelectableListBoundary
- this will define the area over which you can drag to trigger selection in a selectable list view somewhere further down the tree.
In this example we also use ListRowActions
to render row actions & buttons for each row.
// selectionMap is a {[rowKey]: boolean} mapping that represents
// the current state of selected items (true when selected, false or undefined otherwise)
const [selectionMap, setSelectionMap] = useState({});
// onSelectionChange will be called with an updated map when selection changes
const onSelectionChange = (val) => setSelectionMap(val);
const renderSelectableItem = (item) => (
<SelectableListRow rowKey={item.pk}>
<ListRowCell>
<Avatar mr={3} size={36} username={item.owner.username} />
<ListText kind="bold">{item.title}</ListText>
</ListRowCell>
<ListRowActions>
<Button kind="ghost">Some Action</Button>
<Button kind="danger">Delete</Button>
</ListRowActions>
</SelectableListRow>
);
// SelectableListBoundary wraps a Box with some padding and has SelectableListView further down the tree.
// This means the entire area defined by the Box, including the padding, is enabled as a drag area for that list.
return (
<SelectableListBoundary boundaryName="Example Boundary">
<Box padding={6}>
<SelectableListView
onSelectionChange={onSelectionChange}
selectionMap={selectionMap}
>
{projectsSetA.map(renderSelectableItem)}
</SelectableListView>
</Box>
</SelectableListBoundary>
);
To create a table layout, you need to pass a columns
array to ListView
or SelectableListView
.
Each column in the array should have the following properties:
heading
- The text shown as the column’s heading (a string)width
- The width of the column. Any valid grid unit will work - it is recommended to use fr
.loader
- A LoaderCellType
, to describe the loading state placeholder (see infinite scroll example).Each row will inherit the cell widths specified as colWidths
.
As with previous examples, you can group cell content together by wrapping your components with ListRowCell
.
const onSelectionChange = (val) => setSelectionMap(val);
const renderItem = (item) => (
<SelectableListRow rowKey={item.pk} key={item.pk}>
<ListRowCell>
<Avatar mr={3} size={36} username={item.owner.username} />
<ListText kind="bold">{item.title}</ListText>
</ListRowCell>
<ListRowCell>
<ListText>Hello there</ListText>
</ListRowCell>
<ListRowActions>
<Button kind="ghost">Some Action</Button>
<Button kind="danger">Delete</Button>
</ListRowActions>
</SelectableListRow>
);
const columns = [
{ heading: 'User', width: '1fr' },
{ heading: 'Greeting', width: '1fr' },
{ heading: '', width: '1fr' },
];
return (
<SelectableListView
columns={columns}
onSelectionChange={onSelectionChange}
selectionMap={selectionMap}
>
{projectsSetA.map(renderItem)}
</SelectableListView>
);
The list itself has no knowledge of any filtering or sorting methods we use (or might use) in the app. It is simply a set of building blocks for rendering lists. This means that, Any filtering and sorting functionality should be done on the data you use to render your lists. You can combine other UI components to help you achieve this when you are building a more “specialized” list.
const [visibleItems, setVisibleItems] = useState(projectsSetA);
const [searchTerm, setSearchTerm] = useState('');
const onSearchTermChange = term => {
setSearchTerm(term);
if (term) {
// Filter by title matching search term
const filteredItems = visibleItems.filter(item => item.title.toLowerCase().indexOf(term.toLowerCase()) > -1);
setVisibleItems(filteredItems);
} else {
setVisibleItems(projectsSetA);
}
}
// Render your list items as usual
const renderItem = item => ...
return (
<Box>
<SearchInput onChange={onSearchTermChange} value={searchTerm}/>
<ListView>{visibleItems.map(renderListItem)}</ListView>
</Box>
);
To enable infinite scroll on either ListView
or SelectableListView
, pass the onScrollToBottom
prop that will be called when the list is scrolled to the bottom.
Pass the isLoadingRows
prop when you are fetching more data for the list.
Each column can specify its loader type. This will render the correct preloader graphic when the list is loading.
Available loader types are: LoaderCellType.Text
, LoaderCellType.Avatar
and LoaderCellType.AvatarWithText
.
Optionally, use the listStyle
prop to pass additional styles for the list, such as maxHeight
and overflow: scroll
if you need to set the list in a fixed size scrollable container.
If the callback passed to onScrollToBottom
returns a Promise
, the list view will not trigger the callback again as long as the ongoing async callback did not complete.
To display loading states for rows, use the ListRowLoader
component. It takes the following props:
const [items, setItems] = useState(projectsSetA);
const [isLoading, setLoading] = useState(false);
// This function returns a promise and will be called when scrolling to bottom.
// In a real use case, you will be fetching real data here.
const loadMoreItems = () => {
return new Promise((resolve) => {
setLoading(true);
setTimeout(() => {
const newItems = someNewItems;
setItems([...items, ...newItems]);
setLoading(false);
resolve();
}, 3000);
});
};
const renderListItem = (item) => (
<ListRow>
<ListRowCell>
<Avatar mr={3} size={36} username={item.owner.username} />
<ListText kind="bold">{item.title}</ListText>
</ListRowCell>
</ListRow>
);
const columns = [{ loader: LoaderCellType.AvatarWithText }];
return (
<Box borderRadius={2} boxShadow="popover.default" marginTop={4}>
<ListView
columns={columns}
isLoadingRows={isLoading}
listStyle={{ padding: 6, height: '400px', overflow: 'scroll' }}
onScrollToBottom={loadMoreItems}
>
{items.map(renderListItem)}
</ListView>
</Box>
);