Comparison of JavaScript table libraries using Svelte: Bryntum Grid vs. AG Grid vs. DataTables

Bryntum Grid, AG Grid, and DataTables are JavaScript libraries that are used to create tables with advanced functionality. In this tutorial, we’ll create a Svelte app with tables using each of these libraries to compare them. The tables will demonstrate the following features:
- Filtering and sorting.
- Adding and deleting rows.
- Cell editing.
- Row and column reordering.
- Adding summaries.
- Setting the table to read-only.
- Exporting to Excel.
Getting started
We’ll start this project by cloning the following starter GitHub repository. The starter repository is a Svelte app that uses Vite, which is a development server and JavaScript bundler. You’ll need Node.js version 14.18+ for Vite to work. The completed-tables
branch in the GitHub repository contains the completed code for the tutorial.
Now install the dev dependencies by running the following command:
npm install
Next, install Bryntum Grid with npm. Bryntum npm packages are hosted in a private Bryntum registry.
First, follow this set up guide to get access to the Bryntum registry. You can then install the Bryntum Grid dependencies with one of the following commands:
Trial version:
npm install @bryntum/grid@npm:@bryntum/grid-trial@5.2.1
Licensed version:
npm install @bryntum/grid@5.2.1
The starter Svelte app has routing set up using svelte-spa-router so that each table can be viewed on a separate page. There are three routes in the src/routes
folder, one for each table. There is also a basic table set up for each of the table libraries in the src/lib
folder. The tables are all created once the table component is first rendered to the DOM using Svelte’s onMount
method. The table data, which shows data for fictional soccer players, is also fetched after the component is rendered from the public/data/data.json
file.
Now run the local dev server using npm run dev
and you’ll see three card elements that link to pages for the different tables. Click through to view the tables. You’ll see a starter table for each with no data.
In this tutorial, we’ll only change files in the /lib
folder. We’ll add data and features to the tables. Let’s start by adding data and features to the Bryntum Grid table.
Creating a Bryntum Grid table
The Bryntum Grid, which is the grid
variable in the BryntumGrid.svelte
file, has some basic configuration added. It’s appended to the <div>
with an id
of bryntum-grid
that’s in the routes/BryntumGridPage/svelte
file.
Let’s add our table data.
Loading data
Import the following AjaxStore
in the <script>
tag of the BryntumGrid.svelte
file:
import { AjaxStore } from "@bryntum/grid";
Add the following variable below the imports:
const store = new AjaxStore({
readUrl: "/data/data.json",
autoLoad: true,
});
Now add the store to the grid
configuration object:
store,
Bryntum Grid uses a Store
data container to store and manage data in JSON format. The AjaxStore
is a Store
that uses the Fetch API to read and sync data from a remote server. In our code, we read the data from the public
folder using the readUrl
property. The autoLoad
property is set to true
so that the data is automatically loaded when the store is instantiated.
You’ll now see data in the Bryntum Grid table:
The columns are sortable by default. Click on a column heading to sort a row in ascending order. Click it again to sort it in descending order. The columns are also reorderable. Click and drag a column to reorder it.
Now let’s add some more features to the Bryntum Grid table.
Adding filtering and row reordering
Add the following features
property to the grid
configuration object:
features: {
filter: true,
rowReorder: {
showGrip: true,
},
stripe: true,
},
Features add functionality to the grid. The features
property defines what features the grid will use. We added the filter
property to allow the grid to be filtered. The rowReorder
property allows a user to reorder a row by dragging it. The showGrip
property adds a grip icon to the left side of each row. The stripe
property adds stripes by adding alternating background colors to the rows.
We can also add the following property below the features
property to show which cells have been edited:
// Show changed cells
showDirty: true,
This property makes the grid show a red “changed” tag in cells that have had their field value changed and the change has not yet been committed to a backend database.
Now let’s add a custom filter to the “NAME” column.
Adding a custom filter to the name column
In the “name” field object of the columns
property array, add the following method:
// This column has a custom filtering function that matches whole words
filterable: ({ value, record }) =>
Boolean(record.name.match(new RegExp(`${value}\\b`, "i"))),
This function adds a custom filter that matches whole words only. To find the “Dan Jones” record, you’ll need to type out “dan”, “jones”, or “dan jones”. The search is case-insensitive.
You can filter a row by hovering over a column heading and clicking on the filter icon:
Adding add, insert, and remove row buttons
Now let’s add some buttons to the top of our Bryntum Grid to add, insert, or remove buttons. First, add the following variable in the <script>
tag at the top:
let newPlayerCount = 0;
Now add the following tbar
config object to the Bryntum Grid to configure its Toolbar
:
tbar: [
{
type: "buttongroup",
items: [
{
type: "button",
ref: "addButton",
color: "b-green",
icon: "b-fa-plus-circle",
text: "Add",
tooltip: "Adds a new row (at bottom)",
onAction: () => {
const counter = ++newPlayerCount,
added = grid.store.add({
name: `New player ${counter}`,
cls: `new_player_${counter}`,
});
grid.selectedRecord = added[0];
},
},
{
type: "button",
ref: "insertButton",
color: "b-green",
icon: "b-fa-plus-square",
margin: "0 7",
text: "Insert",
tooltip: "Inserts a new row (at top)",
onAction: () => {
const counter = ++newPlayerCount,
added = grid.store.insert(0, {
name: `New player ${counter}`,
cls: `new_player_${counter}`,
});
grid.selectedRecord = added[0];
},
},
{
type: "button",
ref: "removeButton",
color: "b-red",
icon: "b-fa b-fa-trash",
text: "Remove",
tooltip: "Removes selected record(s)",
onAction: () => {
const selected = grid.selectedRecords;
if (selected && selected.length) {
const store = grid.store,
nextRecord = store.getNext(selected[selected.length - 1]),
prevRecord = store.getPrev(selected[0]);
store.remove(selected);
grid.selectedRecord = nextRecord || prevRecord;
}
},
},
],
},
],
We added our buttons to the toolbar as a buttonGroup
of button items. When one of the buttons is pressed, the onAction
callback function is called. It adds, inserts, or removes items from the grid store. The “Remove” button deletes selected rows.
Adding a remove all filters button
The filters for different columns can be applied at the same time. Let’s create a button that will remove all filters in the grid. Add the following object to the tbar
array:
{
type: "button",
ref: "removeAll",
text: "Remove all filters",
margin: "0 5",
onAction: () => store.clearFilters(),
},
This button will clear all filters in the Bryntum Grid store when it’s pressed.
Adding a read-only toggle button
Let’s add a toggle button to set the read-only state of the grid. Add the following object to the tbar
array:
{
type: "button",
ref: "readOnlyButton",
text: "Read-only",
tooltip: "Toggles read-only mode on grid",
toggleable: true,
icon: "b-fa-square",
pressedIcon: "b-fa-check-square",
onToggle: ({ pressed }) => {
addButton.disabled =
insertButton.disabled =
grid.readOnly =
pressed;
removeButton.disabled = pressed || !grid.selectedRecords.length;
},
},
Now, add the following line in the onMount
callback function, below the grid
variable:
const { addButton, removeButton, insertButton } = grid.widgetMap;
The read-only mode is set by toggling the grid.readOnly
property. We also disable the add, insert, and remove buttons when the grid is in read-only mode.
Adding a summary row
To add a summary row at the bottom of the grid, we’ll first import the StringHelper
function:
import { StringHelper } from "@bryntum/grid";
Now, add the following summary
property to the features
property of the grid config:
summary: true,
Next, we’ll add the sum
and summaryRenderer
properties for the columns
. The sum
is the summary type and the summaryRenderer
is the renderer function for the summary. We also use the summaries
property for the score
column because we’ll have more than one summary value for the column.
Add the following properties to the name
column object:
sum: "count",
summaryRenderer: ({ sum }) => `Total: ${sum}`,
Add the following properties to the city
column object:
sum: (result, current, index) => {
if (index === 0) {
result = {};
}
const city = current.city;
if (!Object.prototype.hasOwnProperty.call(result, city)) {
result[city] = 1;
} else {
++result[city];
}
return result;
},
summaryRenderer: ({ sum }) => {
let value = 0,
mostPopularCity = "";
Object.keys(sum).forEach((key) => {
if (value < sum[key]) {
value = sum[key];
mostPopularCity = key;
}
});
return StringHelper.xss`Most entries: ${mostPopularCity} (${value})`;
},
Add the following properties to the team
column object:
sum: (result, current, index) => {
// There is
if (index === 0) {
result = {};
}
const team = current.team;
if (!Object.prototype.hasOwnProperty.call(result, team)) {
result[team] = 1;
} else {
++result[team];
}
return result;
},
summaryRenderer: ({ sum }) => {
let value = 0,
mostPopularTeam = "";
Object.keys(sum).forEach((key) => {
if (value < sum[key]) {
value = sum[key];
mostPopularTeam = key;
}
});
return StringHelper.xss`Most entries: ${mostPopularTeam} (${value})`;
},
Add the following properties to the score
column object:
// Using built in summary calculations
summaries: [
{ sum: "min", label: "Min" },
{ sum: "max", label: "Max" },
],
Add the following properties to the percentWins
column object:
sum: "average",
summaryRenderer: ({ sum }) => `Average: ${Math.round(sum)}%`,
The sum
properties use one of the valid string types or a function for a custom summary.
You’ll now be able to see the summary rows for all of the columns:
Adding a sum selected rows button
Now let’s add a button that will show the summary values for the selected rows. Add the following object to the tbar
array:
{
type: "button",
text: "Sum selected rows",
margin: "0 auto",
toggleable: true,
onToggle: "up.onSelectToggle",
},
Add the onSelectToggle
method below the columns
array in the grid config object:
onSelectToggle() {
this.features.summary.selectedOnly =
!this.features.summary.selectedOnly;
},
When the button is clicked, the summary.selectedOnly
feature is toggled. When it’s true, the summary will be for the selected rows:
Now let’s add an Excel export feature.
Adding Excel export
For the Excel export, we’ll use the third-party library zipcelx. Import it inside the <script>
tag:
import zipcelx from "zipcelx";
Now add the following features to the features
property in the grid config:
excelExporter: {
zipcelx,
},
cellMenu: {
items: {
extraItem: {
text: "Export - Excel",
icon: "b-fa-download",
weight: 200,
onItem: () => {
grid.features.excelExporter.export();
},
},
},
},
We create the excelExporter
and then add it as a menu item to the cellMenu
, which is the menu that’s displayed when you right-click on a cell. When the “Export – Excel” menu item is pressed, it calls the excelExporter.export
method, which downloads an Excel file of the table data. The data does not include the summary data.
We’ve now completed our Bryntum Grid table. Let’s create the same table using AG Grid.
Creating an AG Grid table
We’re using the enterprise version of AG Grid, which is free to use non-commercially, and you’ll see a message about this in your browser developer console. You can see differences between the enterprise and community versions in this comparison table.
The AG Grid table, which is the grid
variable in the AgGrid.svelte
file, has some basic grid options added. In the routes/AgGridPage.svelte
file, the table is appended to the <div>
with an id
of datagrid
.
Now, let’s add data to our table.
Loading data
Import the following client
fetch wrapper in the <script>
tag of the AgGrid.svelte
file:
import { client } from "../utils/fetchWrapper";
In the onMount
callback function, add the following lines below the grid
variable assignment:
client("data/data.json").then(({ data }) => {
gridOptions.api.setRowData(data);
});
We fetch the soccer player data using our fetch wrapper function and then add the data to the grid using api.setRowData
.
You’ll now see data in the AG Grid table:
The columns are reorderable by default. Click and drag a column to reorder it. The columns also have a search input, column pinning, and can be automatically sized. You can find these features by hovering over a column header and clicking on the icon that appears. There is also a CSV or Excel export function. Right-click on a cell to find the export options.
Now let’s add some more features to the AG Grid table.
Adding filtering, cell editing, and row re-ordering
Add the following property to each object that’s returned from the getColumnDefs
function:
editable: true,
This makes the cells editable.
Now add the following property to the first object that’s returned from the getColumnDefs
function:
// only allow non-group rows to be dragged
rowDrag: (params) => !params.node.group,
To enable row dragging on all columns, you set the column property rowDrag
to true for the first column. The rowDrag
is enabled for rows that are not grouped. We do this so that the summary row that we’ll add later does not have row drag enabled.
Let’s set some default column definitions that are applied to all columns. Add the following property in the gridOptions
object:
defaultColDef: {
sortable: true,
filter: true,
resizable: true,
},
Now add the following properties below the defaultColDef
property:
rowSelection: "multiple", // allow rows to be selected
animateRows: true, // have rows animate to new positions when sorted
rowDragManaged: true,
We set the rowDragManaged
property to true
so that the grid is responsible for rearranging the rows when they are reordered by dragging.
You’ll now be able to edit cells, filter columns, and reorder rows:
Now let’s add a custom filter to the “NAME” column, like the one we made for the Bryntum Grid.
Adding a custom filter to the name column
We’ll use a filter component to create a custom filter. In the lib
folder, create a file called personFilter.js
and add the following lines to it:
export class PersonFilter {
init(params) {
this.valueGetter = params.valueGetter;
this.filterText = null;
this.setupGui(params);
}
// not called by AG Grid, just for us to help setup
setupGui(params) {
this.gui = document.createElement("div");
this.gui.innerHTML = `<div style="padding: 4px; width: 200px;">
<div style="font-weight: bold;">Custom filter: whole words</div>
<div>
<input style="margin: 4px 0 4px 0;" type="text" id="filterText" placeholder="Full name search..."/>
</div>
</div>
`;
const listener = (event) => {
this.filterText = event.target.value;
params.filterChangedCallback();
};
this.eFilterText = this.gui.querySelector("#filterText");
this.eFilterText.addEventListener("changed", listener);
this.eFilterText.addEventListener("paste", listener);
this.eFilterText.addEventListener("input", listener);
// IE doesn't fire changed for special keys (eg delete, backspace), so need to
// listen for further ones
this.eFilterText.addEventListener("keydown", listener);
this.eFilterText.addEventListener("keyup", listener);
}
getGui() {
return this.gui;
}
doesFilterPass(params) {
const searchQuery = this.filterText.toLowerCase();
const value = this.valueGetter(params);
console.log({ searchQuery }, { value });
return Boolean(searchQuery.match(new RegExp(`${value}\\b`, "i")));
}
isFilterActive() {
return this.filterText != null && this.filterText !== "";
}
getModel() {
return { value: this.filterText.value };
}
setModel(model) {
this.eFilterText.value = model.value;
}
}
This custom filter class adds a custom filter that matches whole words only.
Import this filter in the <script>
tag of the AgGrid.svelte
file:
import { PersonFilter } from "./personFilter";
Add the following components
property to the gridOptions
object:
components: {
personFilter: PersonFilter,
},
Now set the filter
of the “NAME” column to our custom filter. Add the following property to the first object of the array returned by the getColumnDefs
function:
filter: "personFilter",
The “NAME” column now has a custom whole-word filter.
Adding add, insert, and remove row buttons
We’ll now add buttons to add, insert, and remove a row to the top of the AG Grid table, like we did with the Bryntum Grid table. First, import the following icons:
import FaRegCheckCircle from "svelte-icons/fa/FaRegCheckCircle.svelte";
import FaRegCircle from "svelte-icons/fa/FaRegCircle.svelte";
import FaPlusCircle from "svelte-icons/fa/FaPlusCircle.svelte";
import FaTrash from "svelte-icons/fa/FaTrash.svelte";
Add the following variable below the gridContainer
variable declaration:
let canEdit = true;
We’ll use this variable to determine if the grid is read-only or not so that we can disable editing if the read-only button is toggled on. We’ll be adding this button later.
Add the following toolbar <div>
inside the grid container that has an id
of datagrid
:
<div class="toolbar">
<button
class="btn add-btn"
on:click={() => gridOptions.api.applyTransaction({ add: [{}] })}
disabled={!canEdit}
>
<div class="icon">
<FaPlusCircle />
</div>
<div>ADD</div>
</button>
</div>
Add the following CSS styling for the toolbar and the buttons that we’ll add:
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
background-color: var(--light-grey);
padding: 0.5rem;
}
.btn {
display: flex;
gap: 0.3rem;
align-items: center;
padding: 0.75rem;
border-radius: 2px;
color: var(--dark-grey);
border: 1px solid var(--medium-grey);
transition: all 0.3s ease;
}
.btn:hover {
cursor: pointer;
filter: brightness(115%);
}
.btn:disabled {
opacity: 0.5;
}
.icon {
display: flex;
align-items: center;
width: 12px;
height: 12px;
}
.add-btn {
color: var(--green);
border: 1px solid var(--light-green);
}
.remove-btn {
color: var(--red);
border: 1px solid var(--light-red);
}
We use api.applyTransaction
to add a row when the button is clicked.
Now add the following two buttons below the “ADD” button:
<button
class="btn add-btn"
on:click={() =>
gridOptions.api.applyTransaction({ add: [{}], addIndex: 0 })}
disabled={!canEdit}
>
<div class="icon">
<FaPlusCircle />
</div>
<div>INSERT</div>
</button>
<button
class="btn remove-btn"
on:click={() => {
const selectedRows = gridOptions.api.getSelectedRows();
gridOptions.api.applyTransaction({ remove: selectedRows });
}}
disabled={!canEdit}
>
<div class="icon"><FaTrash /></div>
<div>REMOVE</div></button
>
Now you’ll see “ADD”, “INSERT”, and “DELETE” buttons at the top of the AG Grid table:
Next, let’s add a button that removes all filters applied to the grid.
Adding a remove all filters button
Add the following button below the “REMOVE” button:
<button
class="btn remove-filters-btn"
on:click={() => gridOptions.api.setFilterModel(null)}
>
REMOVE ALL FILTERS</button
>
This “REMOVE ALL FILTERS” button clears the filters by setting the grid’s filter model to null
.
Adding a read-only toggle button
Add the following button below the “REMOVE ALL FILTERS” button:
<button
class="btn read-only-btn"
on:click={() => {
const columnDefs = getColumnDefs();
const newState = canEdit ? false : true;
canEdit = newState;
columnDefs.forEach(function (colDef) {
colDef.editable = newState;
});
gridOptions.api.setColumnDefs(columnDefs);
}}
>
<div class="icon">
{#if canEdit}
<FaRegCircle />
{/if}
{#if !canEdit}
<FaRegCheckCircle />
{/if}
</div>
<div>READ-ONLY</div>
</button>
When the “READ-ONLY” button is clicked, we toggle the canEdit
variable’s state and then loop through each column definition and set the editable
property to the new canEdit
state. We then call the setColumnDefs
method to set new column definitions that have the new editable
state.
Adding a summary row
To add a summary row, add the following property to the gridOptions
object:
groupIncludeTotalFooter: true,
To create a summary for each column, we’ll use the aggFunc
property, which is used for custom aggregation functions, for each column. We’ll add these to the column objects of the array returned from the getColumnDefs
function.
Add the following aggFunct
to the name
column object:
aggFunc: (params) => {
let count = 0;
params.values.forEach((value) => {
count += 1;
});
return `Total: ${count}`;
},
Add the following aggFunct
to the city
column object:
aggFunc: (params) => {
const valueCountObj = {};
params.values.forEach((value) => {
if (!valueCountObj[value]) {
valueCountObj[value] = 1;
} else {
valueCountObj[value] += 1;
}
});
const mostEntries = Object.keys(valueCountObj).reduce(
(a, b) => (valueCountObj[a] > valueCountObj[b] ? a : b),
0
);
return `Most entries: ${mostEntries} (${valueCountObj[mostEntries]})`;
},
Add the following aggFunct
to the team
column object:
aggFunc: (params) => {
const valueCountObj = {};
params.values.forEach((value) => {
if (!valueCountObj[value]) {
valueCountObj[value] = 1;
} else {
valueCountObj[value] += 1;
}
});
const mostEntries = Object.keys(valueCountObj).reduce(
(a, b) => (valueCountObj[a] > valueCountObj[b] ? a : b),
0
);
return `Most entries: ${mostEntries} (${valueCountObj[mostEntries]})`;
},
Add the following aggFunct
to the score
column object:
aggFunc: (params) => {
let min = null;
let max = null;
params.values.forEach((value) => {
if (value < min || min === null) {
min = value;
}
if (value > max || max === null) {
max = value;
}
});
return `Min ${min} Max ${max}`;
},
Add the following aggFunct
to the percentWins
column object:
aggFunc: (params) => {
let count = 0;
let sum = 0;
params.values.forEach((value) => {
count += 1;
sum += Number(value);
});
return `Average: ${Math.round(sum / count)} %`;
},
You’ll now see the summary rows in the completed AG Grid table:
We’ll now create a table using DataTables.
Getting started with DataTables
The DataTable, which is the table
variable in the DatatableJS.svelte
file, has some basic table options added. The paging
option is set to false
and the columns are defined with the columns
property. The table has a search input feature on the top right. In the routes/DataTablesPage/svelte
file, the table is appended to the <div>
with an id
of datatable
.
Now, let’s add data to our table.
Loading data
In the table
variable, add the following property below the paging
property:
ajax: {
url: "/data/data.json",
dataSrc: "data",
},
The ajax
property is used to load data into the table. You’ll now be able to see the data in the table:
Comparing Bryntum Grid, AG Grid, and DataTables
The Bryntum Grid and AG Grid tables had many features that were not easy to replicate with DataTables, for example, adding and deleting rows, row reordering, and editing cells. DataTables has plugins to extend its functionality. There were some issues with using the plugins in Svelte, so the DataTables table had less functionality than the other tables. Bryntum Grid and AG Grid offer more modern-looking tables with more advanced features than DataTables. This can be seen on the respective demo pages of the different libraries.
In terms of documentation, all the libraries have many examples with good search functionality. The Bryntum Grid demos page and trial code download was very useful, as the code demos and source code can all be seen together. This is particularly handy for finding and understanding the demos of the advanced features.
Comparing Bryntum Grid with AG Grid in terms of building the table, some features were easier to add with Bryntum Grid and some features were easier to add with AG Grid. The following table shows which features were easier to add for each table:
Feature | Bryntum Grid | AG Grid |
---|---|---|
Custom filters | ✔️ | |
Adding buttons | ✔️ | |
Adding a summary row | ✔️ | |
Exporting data to Excel | ✔️ |
Adding custom filters, buttons, and a summary row required a lot more code with AG Grid than it did with Bryntum Grid, and Bryntum Grid had more of the features built-in. Exporting the table data to Excel was easier with AG Grid, as it was part of the basic table. Bryntum Grid required a third-party library for the data to be exported to Excel.
In terms of pricing, DataTables is free to use and a once-off fee gets you full access to their forum. However, there are additional costs if you make use of plugins such as the DataTables Editor. Bryntum Grid and AG Grid are commercial software libraries that require the purchase of a license. The licenses also include technical support.
In conclusion, if you’re looking for a feature-full table with many advanced features, Bryntum Grid or AG Grid are good choices. For the tables that we made in this tutorial, Bryntum Grid offered the best developer experience thanks to its many built-in functions for common table features, such as creating custom filters, adding buttons, and adding column summaries.
Next steps
This tutorial gives you a starting point for creating a table using Bryntum Grid, AG Grid, and DataTables. Take a look at the demo pages of each library to see what features you could add to improve the tables: