Grid
Vaadin Grid is a component for displaying tabular data, including various enhancements to grid renderings.
Some of the more complex features of this component are described on separate tabs:
new tab
@state()
private items: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override render() {
return html`
<vaadin-grid .items="${this.items}">
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
`;
}
Note
|
Auto-generated columns in Flow Grid
Although most code examples define columns explicitly, the Flow component can generated them automatically based root-level properties on the bean class if you pass it as an argument to the constructor, e.g. new Grid<>(Person.class);
|
Dynamic Height
Grid has a default height of 400 pixels. It becomes scrollable when items contained in it overflow the allocated space.
In addition to setting any fixed or relative value, the height of a grid can be set by the number of items in the dataset. The grid expands and retracts based on the row count. This feature disables scrolling. It shouldn’t be used with large data sets since it might cause performance issues.
Notice how the height of the rows in the earlier example adjusts because of the text in the Address cells wrapping. With that in mind, click the gray icon at the top right corner of the example below to open it in a new browser tab. Try resizing it, making it narrower and then wider. Notice how the rows are always the same height and that the text doesn’t wrap. Instead, the text is truncated with ellipses.
new tab
<vaadin-grid .items="${this.invitedPeople}" all-rows-visible>
<vaadin-grid-column header="Name" path="displayName" auto-width></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column path="address.phone"></vaadin-grid-column>
<vaadin-grid-column
header="Manage"
${columnBodyRenderer(this.manageRenderer, [])}
></vaadin-grid-column>
</vaadin-grid>
Selection
Grid supports single and multi-select modes. Neither is enabled by default.
Single-Selection Mode
In single-selection mode, the user can select and deselect rows by clicking anywhere on the row.
new tab
@state()
private items: Person[] = [];
@state()
private selectedItems: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override render() {
return html`
<vaadin-grid
.items="${this.items}"
.selectedItems="${this.selectedItems}"
@active-item-changed="${(e: GridActiveItemChangedEvent<Person>) => {
const item = e.detail.value;
this.selectedItems = item ? [item] : [];
}}"
>
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
`;
}
Multi-Select Mode
In multi-select mode, the user can use a checkbox column to select and deselect more than one row — not necessarily contiguous rows. Or the user can select all rows by clicking on the checkbox in the header row — and then un-check the ones they don’t want to be selected, rather than check many, individually.
new tab
@customElement('grid-multi-select-mode')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override render() {
return html`
<vaadin-grid .items="${this.items}">
<vaadin-grid-selection-column></vaadin-grid-selection-column>
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
</vaadin-grid>
`;
}
}
In addition to selecting rows individually, a range of rows can be selected by dragging from one selection checkbox to another, if enabled:
<vaadin-grid .items="${this.items}">
<vaadin-grid-selection-column drag-select></vaadin-grid-selection-column>
...
</vaadin-grid>
Selection Modes in Flow
Each selection mode is represented by a GridSelectionModel
, accessible through the getSelectionModel()
method, which can be cast that to the specific selection model type, SingleSelectionModel
or MultiSelectionModel
. These interfaces provide selection mode specific APIs for configuration and selection events.
To use Grid with Binder
in Flow, you can use asSingleSelect()
or asMultiSelect()
, depending on the currently defined selection mode. Both methods return interfaces that implement the HasValue
interface for use with Binder
.
Sorting
Any column can be used for sorting the data displayed. Enable sorting to allow the user to sort items alphabetically, numerically, by date, or by some other method.
The arrowhead symbols in the column header indicate the current sorting direction. When toggled, the direction will cycle between ascending, descending and none.
new tab
<vaadin-grid .items="${this.items}">
<vaadin-grid-sort-column path="id"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="displayName" header="Name"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="email"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="birthday"></vaadin-grid-sort-column>
</vaadin-grid>
Sorting by Multiple Columns
Multi-sort mode allows the Grid to be sorted by multiple columns simultaneously.
In normal multi-sort mode, additional sorting columns are applied simply by clicking their headers.
A separate multi-sort on shift-click mode combines single and multi-column sorting by adding more sorting columns only when the user holds the Shift key while clicking their headers.
The order in which multi-sort columns (known as sorting criteria) are evaluated is determined by the multi-sort priority setting.
new tab
<vaadin-grid .items="${this.items}" multi-sort multi-sort-priority="append">
<vaadin-grid-sort-column path="id"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="displayName" header="Name"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="email"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="birthday"></vaadin-grid-sort-column>
</vaadin-grid>
Note
|
Shift-Click Multi-Sorting Accessibility Issues
The multi-sort on shift-click mode is not recommended for applications for which accessibility is important. This feature is unlikely to work well with assistive technologies, and the lack of visual affordance makes it difficult to discover for sighted users.
|
Specifying Sort Property
Columns with rich or custom content can be sorted by defining the property by which to sort. For example, in the table here there’s a column containing the employees' first and last names, avatar images, and email addresses. By clicking on the heading for that column, it’ll sort the data by their last names.
new tab
@state()
private items: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people;
}
protected override render() {
return html`
<vaadin-grid .items="${this.items}">
<vaadin-grid-sort-column
header="Employee"
path="lastName"
${columnBodyRenderer(this.employeeRenderer, [])}
></vaadin-grid-sort-column>
<vaadin-grid-column
${columnHeaderRenderer(this.birthdayHeaderRenderer, [])}
${columnBodyRenderer(this.birthdayRenderer, [])}
></vaadin-grid-column>
</vaadin-grid>
`;
}
private employeeRenderer: GridColumnBodyLitRenderer<Person> = (person) => html`
<vaadin-horizontal-layout style="align-items: center;" theme="spacing">
<vaadin-avatar
img="${person.pictureUrl}"
name="${person.firstName} ${person.lastName}"
></vaadin-avatar>
<vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
<span>${person.firstName} ${person.lastName}</span>
<span style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);">
${person.email}
</span>
</vaadin-vertical-layout>
</vaadin-horizontal-layout>
`;
private birthdayHeaderRenderer = () => html`
<vaadin-grid-sorter path="birthday">Birthdate</vaadin-grid-sorter>
`;
private birthdayRenderer: GridColumnBodyLitRenderer<Person> = (person) => {
const birthday = parseISO(person.birthday);
return html`
<vaadin-vertical-layout style="line-height: var(--lumo-line-height-m);">
<span> ${format(birthday, 'P')} </span>
<span style="font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color);">
Age: ${differenceInYears(Date.now(), birthday)}
</span>
</vaadin-vertical-layout>
`;
};
Sorting helps users find and examine data. Therefore, it’s recommended to enable sorting for all applicable columns. An exception, though, would be when the order is an essential part of the data itself, such as with prioritized lists.
Filtering
Filtering allows the user to find a specific item or subset of items. You can add filters to Grid columns or use external filter fields.
For instance, try typing anna
in the input box for Name below. When you’re finished, the data shown is only people who have anna in their name. That includes some with the names Anna and Annabelle, as well as some with Arianna and Brianna.
new tab
<vaadin-grid .items="${this.items}">
<vaadin-grid-filter-column
header="Name"
path="displayName"
flex-grow="0"
width="230px"
${columnBodyRenderer(this.nameRenderer, [])}
></vaadin-grid-filter-column>
<vaadin-grid-filter-column path="email"></vaadin-grid-filter-column>
<vaadin-grid-filter-column path="profession"></vaadin-grid-filter-column>
</vaadin-grid>
Place filters outside the grid when the filter is based on multiple columns, or when a bigger field or more complex filter UI is needed, one which wouldn’t fit well in a column. In the example here, whatever you type in the search box can be matched against all of the columns. Type Rheumatologist
in the search box. The results show only the rows with that profession.
new tab
@state()
private filteredItems: PersonEnhanced[] = [];
private items: PersonEnhanced[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
const items = people.map((person) => ({
...person,
displayName: `${person.firstName} ${person.lastName}`,
}));
this.items = items;
this.filteredItems = items;
}
protected override render() {
return html`
<vaadin-vertical-layout theme="spacing">
<vaadin-text-field
placeholder="Search"
style="width: 50%;"
@value-changed="${(e: TextFieldValueChangedEvent) => {
const searchTerm = (e.detail.value || '').trim();
const matchesTerm = (value: string) =>
value.toLowerCase().includes(searchTerm.toLowerCase());
this.filteredItems = this.items.filter(
({ displayName, email, profession }) =>
!searchTerm ||
matchesTerm(displayName) ||
matchesTerm(email) ||
matchesTerm(profession)
);
}}"
>
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
</vaadin-text-field>
<vaadin-grid .items="${this.filteredItems}">
<vaadin-grid-column
header="Name"
flex-grow="0"
width="230px"
${columnBodyRenderer(this.nameRenderer, [])}
></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
</vaadin-vertical-layout>
`;
}
Lazy Loading
When you want to display a list of items that would be quite large to load entirely into memory, or you want to load items from a database, data providers can be used to provide lazy loading through pagination.
The following example works like the earlier example, but it uses a data provider for lazy loading, sorting, and filtering items.
new tab
async function fetchPeople(params: {
page: number;
pageSize: number;
searchTerm: string;
sortOrders: GridSorterDefinition[];
}) {
const { page, pageSize, searchTerm, sortOrders } = params;
const { people } = await getPeople();
let result = people.map((person) => ({
...person,
fullName: `${person.firstName} ${person.lastName}`,
}));
// Filtering
if (searchTerm) {
result = result.filter(
(p) => matchesTerm(p.fullName, searchTerm) || matchesTerm(p.profession, searchTerm)
);
}
// Sorting
const sortBy = Object.fromEntries(sortOrders.map(({ path, direction }) => [path, direction]));
if (sortBy.fullName) {
result = result.sort((p1, p2) => compare(p1.fullName, p2.fullName, sortBy.fullName));
} else if (sortBy.profession) {
result = result.sort((p1, p2) => compare(p1.profession, p2.profession, sortBy.profession));
}
// Pagination
const count = result.length;
result = result.slice(page * pageSize, pageSize);
return { people: result, count };
}
...
@state()
private searchTerm = '';
@query('#grid')
private grid!: Grid;
private dataProvider = async (
params: GridDataProviderParams<Person>,
callback: GridDataProviderCallback<Person>
) => {
const { page, pageSize, sortOrders } = params;
const { people, count } = await fetchPeople({
page,
pageSize,
sortOrders,
searchTerm: this.searchTerm,
});
callback(people, count);
};
protected override render() {
return html`
<vaadin-vertical-layout theme="spacing">
<vaadin-text-field
placeholder="Search"
style="width: 50%;"
@value-changed="${(e: TextFieldValueChangedEvent) => {
this.searchTerm = (e.detail.value || '').trim();
this.grid.clearCache();
}}"
>
<vaadin-icon slot="prefix" icon="vaadin:search"></vaadin-icon>
</vaadin-text-field>
<vaadin-grid id="grid" .dataProvider="${this.dataProvider}">
<vaadin-grid-sort-column path="fullName" header="Name"></vaadin-grid-sort-column>
<vaadin-grid-sort-column path="profession"></vaadin-grid-sort-column>
</vaadin-grid>
</vaadin-vertical-layout>
`;
}
To learn more about data providers in Flow, see the Binding Items to Components documentation page.
Lazy Column Rendering
Grids containing a large number of columns can sometimes exhibit performance issues. If many of the columns are typically outside the visible viewport, rendering performance can be optimized by using "lazy column rendering" mode.
This mode enables virtual scrolling horizontally. It renders body cells only when their corresponding columns are inside the visible viewport.
Lazy rendering should be used only with a large number of columns and performance is a high priority. For most use cases, though, the default "eager" mode is recommended.
When considering whether to use the "lazy" mode, keep the following factors in mind:
- Row Height
-
When only a number of columns are visible at once, the height of a row can only be that of the highest cell currently visible on that row. Make sure each cell on a single row has the same height as all of the other cells on the row. Otherwise, users may notice jumpiness when horizontally scrolling the grid as lazily rendered cells with different heights are scrolled into view.
- Auto-Width Columns
-
For columns that are initially outside the visible viewport, but still use auto-width, only the header content is taken into account when calculating the column width. This is because the body cells of the columns outside the viewport are not rendered initially.
- Screen Reader Compatibility
-
Screen readers may not be able to associate the focused cells with the correct headers when only a subset of the body cells on a row is rendered.
- Keyboard Navigation
-
Tabbing through focusable elements inside the grid body may not work as expected. This is because some of the columns that would include focusable elements in the body cells may be outside the visible viewport and thus not rendered.
- No Improvement If All Columns Visible
-
The lazy column rendering mode can only improve the rendering performance when a significant portion of the columns are outside of the Grid’s visible viewport. It has no effect on Grids in which all columns are visible without horizontal scrolling.
Item Details
Item details are expandable content areas that can be displayed below the regular content of a row. They can be used to display more information about an item. By default, an item’s details are toggled by clicking on the item’s row. Try clicking on one of the rows in the example here. Notice that when you do, the row is expanded to show the person’s email address, telephone number, and home address. If you click on the row again, it’s collapsed back to a single line.
new tab
@customElement('grid-item-details')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items: Person[] = [];
@state()
private detailsOpenedItem: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people.map((person) => ({
...person,
displayName: `${person.firstName} ${person.lastName}`,
}));
}
protected override render() {
return html`
<vaadin-grid
theme="row-stripes"
.items="${this.items}"
.detailsOpenedItems="${this.detailsOpenedItem}"
@active-item-changed="${(event: GridActiveItemChangedEvent<Person>) => {
const person = event.detail.value;
this.detailsOpenedItem = person ? [person] : [];
}}"
${gridRowDetailsRenderer<Person>(
(person) => html`
<vaadin-form-layout .responsiveSteps="${[{ minWidth: '0', columns: 3 }]}">
<vaadin-text-field
label="Email address"
.value="${person.email}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="Phone number"
.value="${person.address.phone}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="Street address"
.value="${person.address.street}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="ZIP code"
.value="${person.address.zip}"
readonly
></vaadin-text-field>
<vaadin-text-field
label="City"
.value="${person.address.city}"
readonly
></vaadin-text-field>
<vaadin-text-field
label="State"
.value="${person.address.state}"
readonly
></vaadin-text-field>
</vaadin-form-layout>
`,
[]
)}
>
<vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
`;
}
}
The default toggle behavior can be replaced by programmatically toggling the details visibility, such as from a button click.
new tab
@customElement('grid-item-details-toggle')
export class Example extends LitElement {
protected override createRenderRoot() {
const root = super.createRenderRoot();
// Apply custom theme (only supported if your app uses one)
applyTheme(root);
return root;
}
@state()
private items: Person[] = [];
@state()
private detailsOpenedItems: Person[] = [];
protected override async firstUpdated() {
const { people } = await getPeople();
this.items = people.map((person) => ({
...person,
displayName: `${person.firstName} ${person.lastName}`,
}));
}
protected override render() {
return html`
<vaadin-grid
theme="row-stripes"
.items="${this.items}"
.detailsOpenedItems="${this.detailsOpenedItems}"
${gridRowDetailsRenderer<Person>(
(person) => html`
<vaadin-form-layout .responsiveSteps="${[{ minWidth: '0', columns: 3 }]}">
<vaadin-text-field
label="Email address"
.value="${person.email}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="Phone number"
.value="${person.address.phone}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="Street address"
.value="${person.address.street}"
colspan="3"
readonly
></vaadin-text-field>
<vaadin-text-field
label="ZIP code"
.value="${person.address.zip}"
readonly
></vaadin-text-field>
<vaadin-text-field
label="City"
.value="${person.address.city}"
readonly
></vaadin-text-field>
<vaadin-text-field
label="State"
.value="${person.address.state}"
readonly
></vaadin-text-field>
</vaadin-form-layout>
`,
[]
)}
>
<vaadin-grid-column path="displayName" header="Name"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
<vaadin-grid-column
${columnBodyRenderer<Person>(
(person) => html`
<vaadin-button
theme="tertiary"
@click="${() => {
const isOpened = this.detailsOpenedItems.includes(person);
this.detailsOpenedItems = isOpened
? this.detailsOpenedItems.filter((p) => p !== person)
: [...this.detailsOpenedItems, person];
}}"
>
Toggle details
</vaadin-button>
`,
[]
)}
></vaadin-grid-column>
</vaadin-grid>
`;
}
}
Tooltips can be used as a lightweight alternative to the item details panel.
Context Menu
You can use Context Menu to provide shortcuts for the user. It appears on a right-click by default. In a mobile browser, a long press opens the menu. In the example here, try right-clicking on one of the rows. You’ll notice a box appears with a list of choices: Edit the row, delete it, email the person, or call them. If this example were fully configured, the latter two would open the related application (i.e., the default email program or a telephone application).
Using a context menu shouldn’t be the only way of accomplishing a task, though. The same functionality needs to be accessible elsewhere in the UI. See the documentation page on Context Menu for more information.
new tab
private renderMenu: ContextMenuLitRenderer = (context, menu) => {
const { sourceEvent } = context.detail as { sourceEvent: Event };
const grid = menu.firstElementChild as Grid<Person>;
const eventContext = grid.getEventContext(sourceEvent);
const person = eventContext.item!;
const clickHandler = (_action: string) => () => {
// console.log(`${action}: ${person.firstName} ${person.lastName}`);
};
return html`
<vaadin-list-box>
<vaadin-item @click="${clickHandler('Edit')}">Edit</vaadin-item>
<vaadin-item @click="${clickHandler('Delete')}">Delete</vaadin-item>
<hr />
<vaadin-item @click="${clickHandler('Email')}">Email (${person.email})</vaadin-item>
<vaadin-item @click="${clickHandler('Call')}">Call (${person.address.phone})</vaadin-item>
</vaadin-list-box>
`;
};
protected override render() {
return html`
<vaadin-context-menu ${contextMenuRenderer(this.renderMenu, [])}>
<vaadin-grid .items="${this.items}" @vaadin-contextmenu="${this.onContextMenu}">
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
</vaadin-context-menu>
`;
}
onContextMenu(e: MouseEvent) {
// Prevent opening context menu on header row.
if ((e.currentTarget as Grid).getEventContext(e).section !== 'body') {
e.stopPropagation();
}
}
Tooltips
Tooltips on cells can be useful in many situations: They can be used to give more details on the contents of a cell — if an item details panel would be overkill or otherwise undesirable. They can show the full text of a cell if it’s too long to fit feasibly into the cell itself — if wrapping the cell contents is insufficient or otherwise undesirable. Or they can give textual explanations for non-text content, such as status icons.
In the example here, hold your mouse pointer over the birthday date for one of the rows. A tooltip should appear indicating the age of the person. Now hover over one of the status icons, an X or a checkmark. It’ll use Tooltips to interpret the meaning of the icons.
new tab
private tooltipGenerator = (context: GridEventContext<Person>): string => {
let text = '';
const { column, item } = context;
if (column && item) {
switch (column.path) {
case 'birthday':
text = `Age: ${differenceInYears(Date.now(), parseISO(item.birthday))}`;
break;
case 'status':
text = item.status;
break;
default:
break;
}
}
return text;
};
...
<vaadin-tooltip slot="tooltip" .generator="${this.tooltipGenerator}"></vaadin-tooltip>
See the Tooltips documentation page for details on tooltip configuration.
Cell Focus
Many of the explanations and examples above alluded to giving the focus to rows and cells. Cells can be focused by clicking on a cell, or with the keyboard. The following keyboard shortcuts are available with Grid:
Keys | Action |
---|---|
Tab | Switches focus between sections of the grid (i.e., header, body, footer). |
Left, Up, Right, and Down Arrow Keys | Moves focus between cells within a section of the grid. |
Page Up | Moves cell focus up by one page of visible rows. |
Page Down | Moves cell focus down by one page of visible rows. |
Home | Moves focus to the first cell in a row. |
End | Moves focus to the last cell in a row. |
The cell focus event can be used to be notified when the user changes focus between cells. By default, the focus outline is only visible when using keyboard navigation. For illustrative purposes, the example below also uses custom styles to show the focus outline when clicking on cells. Try clicking on a cell. Notice how the cell is highlighted and notice the information shown at the bottom, the information provided about the event.
new tab
protected override render() {
return html`
<vaadin-grid
class="force-focus-outline"
.items="${this.items}"
@cell-focus="${(e: GridCellFocusEvent<Person>) => {
const eventContext = this.grid.getEventContext(e);
const section = eventContext.section ?? 'Not available';
const row = eventContext.index ?? 'Not available';
const column = eventContext.column?.path ?? 'Not available';
const person = eventContext.item;
const fullName =
person?.firstName && person?.lastName
? `${person.firstName} ${person.lastName}`
: 'Not available';
this.eventSummary = `Section: ${section}\nRow: ${row}\nColumn: ${column}\nPerson: ${fullName}`;
}}"
>
<vaadin-grid-column path="firstName"></vaadin-grid-column>
<vaadin-grid-column path="lastName"></vaadin-grid-column>
<vaadin-grid-column path="email"></vaadin-grid-column>
<vaadin-grid-column path="profession"></vaadin-grid-column>
</vaadin-grid>
<div>
<vaadin-text-area
label="Cell focus event information"
readonly
.value="${this.eventSummary}"
></vaadin-text-area>
</div>
`;
}
Related Components
Component | Usage Recommendation |
---|---|
Component for creating, displaying, updating, and deleting tabular data. | |
Component for showing and editing tabular data. | |
Component for showing hierarchical tabular data. | |
Lightweight component for lightweight, single-column lists. |
AC63AABF-4102-4C3E-9776-A09DDC04EF37