VueDataTable
is a Vue plugin to easily create fully-featured data tables.
Check out my other plugin, vue-form-builder, that automatically generates beautiful forms from declarative rules.
- Pagination
- Search filter
- Single column sorting
- Multiple column sorting
- Customize every visible text
- Support for multiple languages
- Export data (JSON, CVS, TXT or XLS)
- Action buttons (view, edit, delete)
- Editable cells (edit cell values)
- Custom Vue components to render cells
- Custom Footer to display data summary
- Support for Vue3 and Vue2
- Nuxt integration
- Laravel integration
The best way to see if a package suits your needs is by viewing and testing its functionalities via a demo app.
There is also this CodeSandbox Playground in which you can edit the source code with live preview.
npm install @uwlajs/vue-data-table
Make sure to install version 2.0.0
or above for Vue3.
Versions prior to 2.0.0
are for Vue2. Checkout the vue2
branch for its documentation.
import VueDataTable from "@uwlajs/vue-data-table";
Vue.component("vue-data-table", VueDataTable);
Don"t forget to add the style sheets
import "@uwlajs/vue-data-table/dist/VueDataTable.css";
<template>
<div>
<data-table v-bind="bindings"/>
</div>
</template>
<script>
export default {
computed: {
bindings() {
return {
columns: [/*the columns*/]
data: [/*the data*/]
/* other props...*/
}
}
},
}
</script>
Note Notice that v-bind will take all key-value pairs in the object (in this
case, the bindings
), and pass them as props to the VueDataTable.
So, this is
a shortcut to pass multiple props at once.
Create a file @/plugins/vue-data-table.js
, or whatever name you wish, with the following content:
import VueDataTable from '@uwlajs/vue-data-table'
import '@uwlajs/vue-data-table/dist/style.css'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(VueDataTable)
})
Nuxt automatically loads the files in the plugins/
directory by default.
This plugin integrates with Laravel's pagination API, so it fetches data asynchronously from the provided URL. Follow the instrunctions in the async data section for a detailed setup.
Only columns
are required. Other props are optional.
If data
is not passed, then fetchUrl
and fetchCallback
must be passed.
vKey
is not required but is highly recommend to set it if you plan to
add or delete rows in the table!
prop | type | default | description |
---|---|---|---|
allowedExports | Array |
["csv", "json", "txt"] |
Formats the user can export the data to. Allowed values: csv , json , txt , xlsx |
data | Array |
- | Array of objects with the data to be displayed on the table |
columns | Array |
- | Array of objects to specify how to render each column. Optional if columnKeys is set |
columnKeys | Array |
- | Array of strings matching the object keys in data . Discarded if columns is set |
lang | String |
en |
The default language |
perPageSizes | Array |
[10, 25, 50, 100, '*'] | The options for the number of rows being displayed per page. The string '*' shows all. |
defaultPerPage | Number |
10 | The default number of entries. If unset, then it will be the first value of perPageSizes |
fetchUrl | String |
- | The URL to fetch data from if data is null |
fetchCallback | String |
- | Async function which takes an URL and returns data matching Laravel's pagination API |
isLoading | Bool |
false |
Whether table data is loading. Table rows are shown only if this value is set to false |
loadingComponent | String , Object |
- | VueJS component to be shown if isLoading is set to true |
showPerPage | Bool |
true |
Whether to show the PerPage component |
showEntriesInfo | Bool |
true |
Whether to show the EntriesInfo component |
showSearchFilter | Bool |
true |
Whether to show the SearchFilter component |
showPagination | Bool |
true |
Whether to show the Pagination component |
showDownloadButton | Bool |
true |
Whether to show the button to download the table's data |
tableClass | String |
table table-striped table-hover |
The table's HTML class attribute |
sortingMode | String |
multiple |
Whether to sort a single column or multiple columns at once |
sortingIndexComponent | String , Object |
VdtSortingIndex |
VueJS component for the sorting index on sortable columns |
sortingIconComponent | String , Object |
VdtSortingIcon |
VueJS component for the sorting icon on sortable columns |
footerComponent | String , Object |
null |
VueJS component for custom table footer |
vKey | String |
- | The v-key , the key in data used by Vue to track and distinguish array elements. |
key | type | default | description |
---|---|---|---|
key | String |
- | The object field to be displayed in a table cell |
title | String |
titleCase(key) |
The title displayed in the header |
searchable | Bool |
true |
Whether to allow searching rows by this column field |
sortable | Bool |
true |
Whether to allow sorting the data by this column field |
editable | Bool |
true |
Whether the column is editable by the user |
collapsible | Bool |
false |
Whether the column is collapsible (expand and collapse) |
type | String |
string |
Data type of key . Allowed values: string , number |
compareFunction | Function |
- | Custom function provided by the user to sort the column |
searchFunction | Function |
- | Custom function provided by the user to search the column |
index | Number |
1000 | Lower values shift the column to the left of the table |
component | String , Object |
- | Custom Vue component to render inside table cell |
componentProps | Object |
- | Props to pass to the custom component |
If columns
is not defined, then columnKeys
must be defined and it will be
mapped to a columns
array with the default parameters. Example:
// we can define the columns
config = {
data: users,
columns: [
{
key: "name",
},
{
key: "email",
title: "Email Address",
sortable: false,
},
{
key: "phone",
sortable: false,
searchable: false,
index: 1, // smaller indexes means the column is shift to the left
},
{
key: "permissions",
/* custom function sort users by which user has more permissions */
compareFunction: function(a, b) {
// permissions is an array
return a.permissions.length - b.permissions.length;
},
/* custom function to allow searching the permission array */
searchFunction: function(search, data) {
return data.permissions.some(permission => permission.includes(search))
},
searchable: true,
/* custom component to display the permissions */
component: UserPermissionList,
}
]
}
// or use columnKeys shortcut
config = {
data: user,
columnKeys: ["name", "email", "registered_at", "last_access_at"]
},
// which will take the default column and map the array into this
[
{
key: "name",
title: "Name",
sortable: true,
searchable: true,
index: 1000
},
{
key: "email",
title: "Email",
sortable: true,
searchable: true,
index: 1000
},
{
key: "registered_at",
title: "Registered At",
sortable: true,
searchable: true,
index: 1000
},
{
key: "last_access_at",
title: "Last Access At",
sortable: true,
searchable: true,
index: 1000
},
]
Custom cell components must have a data
property to receive the data of the current
row for the component to display it.
In the previous code snippet, we used our custom component UserPermissionList
.
Below is a sample of that custom component.
<template>
<div>
List of permissions for the user {{ data.name }} :
<ul>
<li v-for="(permission, i) in data.permissions" :key="i">
{{ permission }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: "UserPermissionList",
props: {
data: {
type: Object,
required: true
}
}
}
</script>
To handle events triggered by a custom component (such as clicking a button in a
component), the component should emit an event called userEvent
and pass an
arbitrary payload to it. The event will be propagated upwards by VueDataTable
,
which will also emit an event called userEvent
whose payload is the same as
the one emitted by the custom component. For example:
<template>
<input type="checkbox" class="form-control" :checked="value" @change='toggleChecked' />
</template>
<script>
export {
name: 'CheckboxCell',
data() {
return {
value: false,
}
},
methods: {
toggleChecked() {
const payload = {
id: this.data.id,
checked: this.value,
}
this.$emit('userEvent', payload)
}
},
props: {
data: Object,
}
}
</script>
When the users clicks the checkbox, it will emit an userEvent
event, which can
be accessed from the VueDataTable
. Here is an continuation of the previous
example.
<template>
<div class="dashboard">
<h1>DASHBOARD</h1>
<button class="btn btn-danger">
DELETE SELECTED ROWS
</button>
<vue-data-table
:data="data"
:columns="columns"
@userEvent="handleEvent" />
</div>
</template>
<script>
export default {
data() {
return {
data: [/**/],
columns: [/**/],
selectedRows: [],
}
},
methods: {
handleEvent(payload) {
const { checked, id } = payload
if (checked === true) {
if (! this.selectedRows.includes(id))
this.selectedRows.push(id)
} else {
this.selectedRows = this.selectedRows.filter(rowId => rowId !== id)
}
},
deleteRows() {
this.data = this.data.filter(row => ! this.selectedRows.includes(row.id))
this.selectedRows = []
}
}
}
</script>
In the code snippet above, when the user checks the checkbox rendered by the
custom component CheckboxCell
, it will emit an event that is handled by the
method handleEvent
. This method will add/remove the id
of the row to/from
the selectedRows
array. When the user clicks the "dangerous delete button", it
will deleted the selected rows from the table (on the client side only).
VueDataTable
provides a component called VdtActionButtons
, which can be used
to display buttons for common CRUD action such as viewing, editing, deleting.
Here is an example with all buttons (view, edit, delete) in one column:
<template>
<main>
<h1>DASHBOARD</h1>
<vue-data-table v-bind="params" @userEvent="handleUserEvent"/>
</main>
</template>
<script>
import { VdtActionButtons } from '@uwlajs/vue-data-table'
export default {
data() {
return {
params: {
data: users,
columns: [
{ key: 'name' },
{ key: 'job' },
{ component: VdtActionButtons, title: "actions" },
],
},
}
},
methods: {
handleUserEvent(payload) {
console.log(payload.action, payload.data.name)
}
}
}
</script>
Another example, this time one button per column:
<template>
<main>
<h1>DASHBOARD</h1>
<vue-data-table v-bind="params" @userEvent="handleUserEvent"/>
</main>
</template>
<script>
import { VdtActionButtons } from '@uwlajs/vue-data-table'
export default {
data() {
return {
params: {
data: users,
columns: [
{ key: 'name' },
{ key: 'job' },
{
title: "view"
component: VdtActionButtons,
componentProps: { actions: ["view"] },
},
{
title: "edit"
component: VdtActionButtons,
componentProps: { actions: ["edit"] },
},
{
title: "delete"
component: VdtActionButtons,
componentProps: { actions: ["delete"] },
},
],
},
}
},
methods: {
handleUserEvent(payload) {
console.log(payload.action, payload.data.name)
}
}
}
</script>
When an user click an action button, VueDataTable
will emit an event whose
payload is an object with two fields: action
and data
. The action
is the
name of the action (view, edit, delete) and data
is the data of the row.
Check out the demo to see a real working example of using action buttons.
It is possible to make a column editable by settings editable
to true in the
column definition.
columns: {
[ 'key': name, editable: true],
[ 'key': email, editable: true],
[ 'key': phone, editable: true],
// ...
}
This will make VueDataTable
display an edit
button on the right side of the
cell's text. When the user clicks the button, it will show an input, so the user
can enter a new value for the cell. The user can cancel the editing or confirm.
If the user confirms editing, VueDataTable
will emit a userEvent
whose
payload looks like the following:
{
action: 'updateCell',
key: '<key String>',
data: '<data Object>',
value: '<value String>',
}
Where key
is the key of the column (if user edits the name
column, the key
will be name
), the data
is the object of the row which was edit (an example:
{ id: 100, name: 'joe', email: '[email protected]' }
), and value
is the value
inserted by the user (such as Joe Doe
).
It is up to the developer to handle the event to update the row by, for example,
sending an AJAX request to the API, then updating the data
array on the client
side. Here is an example of how to update the data array on the client side:
<template>
<vue-data-table :data="data" :columns="columns" @userEvent="handleUserEvent"/>
</template>
<script>
export default {
/* ... */
methods: {
handleUserEvent(payload) {
if (payload.action === 'updateCell')
{
// send some ajax request
// ...
// then update the cell
this.updateDataCell(payload.data, payload.key, payload.value)
} else {
// some other event
}
},
updateDataCell(row, field, value) {
let ind = this.data.findIndex(r => r.id === row.id)
if (ind < 0) return
let newRow = {... this.data[ind]}
newRow[field] = value
this.data.splice(ind, 1, newRow)
},
}
}
</script>
VueDataTable
provides the built-in vdt-cell-selectable
component to select
table rows.
const props = {
columns = [
{
title: "",
component: "vdt-cell-selectable" // <-- ADD THIS
},
{ key: "name" },
{ key: "email" },
/* ... */
],
vKey = "id",
};
const data = [
{ id: 1, name: "joe", email: "[email protected]" },
/* ... */
]
When the user toggles the checkbox, VueDataTable
emits an event called
userEvent
with the following payload:
{
action: "select",
selected: true || false, // this is the current value of the checkbox
data: {}, // this is the current row (example, a user from an users array)
}
You can have a reactive variable to keep track of selected items:
const selected = ref([]);
const handleSelect(payload) {
const item = payload.data;
if (payload.selected) {
selected.value.push(item);
} else {
selected.value = selected.value.filter((x) => x.id !== item.id);
}
}
You can use this variable to perform bulk operations, such as mass deletion or mass edition.
Currently, VueDataTable
has support for the following languages: English (en),
Brazilian Portuguese (pt-br), and Spanish(es). The lang
prop specifies in
which language to display the text in our table.
If we want to add a custom text (maybe because there is no language support or
because we want something else), we have to set it in the text
prop.
The following table shows the texts we can customize and their default values for the English language.
key | default |
---|---|
perPageText | "Show :entries entries" |
perPageAllText | "ALL" |
infoText | "Showing :first to :last of :total entries" |
infoAllText | "Showing all entries" |
infoFilteredText | "Showing :first to :last of :filtered (filtered from :total entries)" |
nextButtonText | "Next" |
previousButtonText | "Previous" |
paginationSearchText | "Go to page" |
paginationSearchButtonText | "GO" |
searchText | "search:" |
downloadText | "export as:" |
downloadButtonText | "DOWNLOAD" |
emptyTableText | "No matching records found" |
Note: Notice that the placeholders :first
, :last
, :total
, and
:filtered
will be automatically replaced with the proper numbers.
Example code:
parameters() {
return {
data: [/**/],
columns: [/**/],
text: {
PerPageText: "Number of users per page :entries",
infoText: "Displaying :first to :last of :total users",
emptyTableText: "No users found :(",
}
}
}
If your language is not yet supported, you can add a new language and use it in
any VueDataTable
instance as follow:
import { languageServiceProvider } from "@uwlajs/vue-data-table";
const loremIpsumLanguage = {
perPageText: "lorem ipsum",
nextButtonText: "labore ipsum",
/* more ... */
};
languageServiceProvider.setLang("lorem", loremIpsumLanguage)
You can also change any default text for an existing language and that will reflect the changes globally. For example:
// the default text for the download button in the export component is "export as"
// we may want change that to "download as"
languageServiceProvider.setLangText("en", "downloadText", "download as:")
If you do not want to fetch all data at once and pass it to VueDataTable
via
the data
prop, you can do so by defining:
fetchUrl
: initial endpoint for the first ajax request to fetch datafetchCallback
: async function which takes an URL and returns a response following Laravel's pagination API.
Here is a sample fetchCallback
:
<template>
<h1>Users</h1>
<vue-data-table v-bind="vdtProps" />
</template>
<script setup>
const vdtProps = {
columns: [
{ key: 'name' },
{ key: "email", title: "Email address" },
],
fetchUrl: "http://app.test/api/users",
fetchCallback: async (url) => fetch(url).then(response => response.json())
}
</script>
The example above uses the browser's built-in fetch
, but you can also use
axios
or Nuxt's $fetch
under the hood. Just make sure the response returned
by the callback matches the following.
The response from the fetchCallback
should look like this:
{
"data": [
{ "id": 1, "name": "Miss Juliet Heidenreich", "email": "[email protected]"},
{ "id": 2, "name": "Heloise Boehm", "email": "[email protected]"},
{ "id": 3, "name": "Antwon Collins", "email": "[email protected]},
/* ... */
],
"current_page": 1,
"first_page_url": "http://app.test/api/users?page=1",
"from": 1,
"last_page": 23,
"last_page_url": "http://app.test/api/users?page=23",
"links": [
{ "url": null, "label": "« Previous", "active": false },
{ "url": "http://app.test/api/users?page=1", "label": "1", "active": true },
{ "url": "http://app.test/api/users?page=2", "label": "2", "active": false },
{ "url": "http://app.test/api/users?page=3", "label": "3", "active": false },
/* ... */
{ "url": "http://app.test/api/users?page=23", "label": "23", "active": false },
{ "url": "http://app.test/api/users?page=2", "label": "Next »", "active": false }
],
"next_page_url": "http://app.test/api/users?page=2",
"path": "http://app.test/api/users",
"per_page": 15,
"prev_page_url": null,
"to": 15,
"total": 340
}
Here is how you do so in Laravel:
<?php
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('users', function () {
return User::paginate();
});
This will also work with collections: new UserCollection(User::paginate())
.
In order to be able to sort and search using endpoints compatible with Laravel's API, this plugin provides support for Spatie's Laravel Query Builder package, which allows you to easily generate API endpoints with sorting and searching functionalities with well-defined standard.
<?php
use App\Models\User;
use Illuminate\Support\Facades\Route;
use Spatie\QueryBuilder\QueryBuilder;
Route::get('users', function () {
return QueryBuilder::for (User::class)
->allowedSorts(['name', 'email'])
->allowedFilters(['name', 'email'])
->paginate();
});
The endpoints look like this:
http://app.test/api/users?page=1&filter[name]=foo
http://app.test/api/users?page31&sort=job,-email
http://app.test/api/users?page=1&sort=email&filter[email]=joe&filter=[name]=joe
You do not need to worry about the URLs if you are using Spatie's Laravel Query Bulder,
because VueDataTable
follows their endpoint standard and automatically generates the urls.
If you do not use their package, then you should parse the url
variable inside
the fetchCallback
, and modify the url. For example, your javascript code should modify:
http://app.test/api/users?page=4&filter[name]=foo --> http://app.test/api/users?page=4&search=foo
.
Keep in mind that, by default, Spatie's Query Builder apply AND logic for all
filters. That means if you have &filter[name]=Ana&filter[email]=Ana
, then
you will only get results that both name
and email
fields match Ana. If
name
matches Ana but not the email
column, then this row would not appear.
Here is how you can implement OR
logic using their package:
<?php
// routes/app.php
use App\Http\Filters\FilterOrWhere;
use App\Models\User;
use Illuminate\Support\Facades\Route;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
Route::get('users', function () {
return QueryBuilder::for (User::class)
->allowedSorts(['name', 'email'])
->allowedFilters([
AllowedFilter::custom('name', new FilterOrWhere),
AllowedFilter::custom('email', new FilterOrWhere)
])
->paginate();
});
// app/Http/Filters/FilterOrWhere.php
namespace App\Http\Filters;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class FilterOrWhere implements Filter
{
public function __invoke(Builder $query, $value, string $property)
{
$query->orWhere($property, 'LIKE', '%' . $value . '%');
}
}
VueDataTable
uses CSS's grid display to specify the position of its components
(search filter, pagination, entries info, per page options, download button).
We can specify the position of the components by including our custom CSS/SCSS and overriding the defaults.
By default, this is how VueDataTable
displays the components:
.data-table {
display: grid;
width: 100%;
grid-template-columns: 25% 25% 25% 25%;
&> div {
margin-top: 1rem;
max-width: 100%;
}
& > .data-table-search-filter, .data-table-pagination, .data-table-export-data {
margin-left: auto
}
@media (min-width: 1401px) {
grid-template-areas:
"perPage search search search"
"table table table table"
"info pagination pagination download";
}
@media (min-width: 1051px) AND (max-width: 1400px) {
grid-template-areas:
"perPage search search search"
"table table table table"
"info pagination pagination pagination"
". . download download";
}
@media (min-width: 851px) AND (max-width: 1050px) {
grid-template-areas:
"perPage search search search"
"table table table table"
"pagination pagination pagination pagination"
"info info download download";
}
@media (max-width: 800px) {
& > .data-table-pagination {
flex-wrap: wrap;
}
}
@media (min-width: 651px) AND (max-width: 850px) {
grid-template-areas:
"perPage search search search"
"table table table table"
"pagination pagination pagination pagination"
"info info info info"
"download download download download";
}
@media (max-width: 650px) {
grid-template-areas:
"search search search search"
"perPage perPage perPage perPage "
"table table table table"
"pagination pagination pagination pagination"
"info info info info"
"download download download download";
& > .data-table-per-page {
margin-left: auto
}
}
}
Feel free to copy the styles above, modify it, and then set the position of the components as you want.
Besides a custom component for each column, you provide custom components for
the table's footer, the column's sorting icon
(the icon displayed if the
columns is sorted), and the column's sorting index
(the index of the current
column if it is being sorted and multi column sorting is enabled).
The property footerComponent
sets the component to render the table's footer.
The component can be either the component Object
, or a String
equals to the
name of the registered component.
The footerComponent
must be a <tfoot>
HTML element and it must have the
properties data
, dataDisplayed
, dataFiltered
. If the component does not
specify those properties in props
, Vue will probably think they are some
custom HTML attribute and their values will be show as HTML attributes, which is
really messy.
The property data
correspond to all data passed to VueDataTable
. The
dataDisplayed
corresponds to all data that is currently visible on the table.
The dataFiltered
corresponds to all data that was filtered by a search query.
These properties can be used to perform common operations such as calculating
the sum of the values of the total rows of a certain column.
Suppose we have a table that of fruits. The data
is an array of objects whose
properties are name
, price
, and amount
. We can provide a custom footer to
show the total amount of fruits bought and the total price.
The footer component would be something like:
<template>
<tfoot v-show="dataDisplayed.length > 0">
<td>Total</td>
<td></td>
<td>{{ totalAmount }}</td>
<td>{{ totalPrice }}</td>
</tfoot>
</template>
<script>
export default {
name: "TableFooter",
computed: {
totalPrice() {
let s = 0;
for (let f of this.dataDisplayed)
s += f.price * f.amount;
return s;
},
totalAmount() {
let s = 0;
for (let f of this.dataDisplayed)
s += f.amount;
return s;
}
},
props: {
data: Array,
dataDisplayed: Array,
dataFiltered: Array,
}
}
</script>
And we pass this component as follow:
<template>
<data-table v-bind="tableProps"/>
</template>
<script>
import TableFooter from './TableFooter.vue'
export default {
/* ... some code */
data() {
return {
tableProps: {
columns: [ /* ... code */ ],
data: [ /* ... more code */ ],
footerComponent: TableFooter,
}
}
}
}
</script>
Alternately, you can register the component and pass a string:
/* early on */
import TableFooter from './TableFooter.vue'
Vue.component("table-footer", TableFooter)
/* later on */
footerComponent: "table-footer"
By default, VueDataTable
will display arrows to indicate the sorting direction
when sorting a column. The SortingIcon
component is wrapped in a th
element.
The th
element has a data-sorting
attribute that may be asc
or desc
only. Based on this value, we display an arrow_up
or an arrow_down
icon
using CSS
rules.
<template>
<span class="data-table-sorting-icon"> </span>
</template>
<style lang="scss" scoped>
.data-table-sorting-icon {
&::after {
content: "\2193";
}
&::before {
content: "\2191";
}
&::after, &::before {
opacity: 0.5;
}
[data-sorting="asc"] &::before, [data-sorting="desc"] &::after {
opacity: 1;
}
}
</style>
Note: Some code was omitted to keep it clean.
If we want to add our custom icons for this, then we can register our component, like so:
import SortingIcon from "./path/to/SortIcon.vue";
export default {
computed: {
bindings() {
return {
SortingIconComponent: SortingIcon,
data: [],
/**/
};
}
}
}
When sorting multiple columns, VueDataTable
will display an icon with a index
indicating which column has the priority in the sorting process.
<template>
<span class="data-table-sort-index">
{{ index }}
</span>
</template>
If we want to add our own component for this, we can register it just like we did before.
import SortingIndex from "./path/to/SortingIndex.vue";
export default {
computed: {
bindings() {
return {
SortingIndexComponent: SortingIndex,
data: [],
/**/
};
}
}
};
In our SortingIndex
component, we must have a index
property, which
correspondent to the index of the column in the sorting process.
export default {
name: "SortingIndex",
props: {
index: {
type: Number,
required: true
}
}
};
- Support for Vue3
- Laravel integration
- Support for SSR
- String notation for defining columns
MIT
Pull requests are very welcome.