- Notifications
You must be signed in to change notification settings - Fork21
uwla/vue-data-table
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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 itsfunctionalities via ademo app.
There is also thisCodeSandbox Playgroundin which you can edit the source code with live preview.
npm install @uwlajs/vue-data-table
Make sure to install version2.0.0
or above for Vue3.
Versions prior to2.0.0
are for Vue2. Checkout thevue2
branch for its documentation.
importVueDataTablefrom"@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-tablev-bind="bindings"/> </div></template><script>exportdefault { 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 thiscase, thebindings
), and pass them as props to theVueDataTable.
So, this isa shortcut to pass multiple props at once.
Create a file@/plugins/vue-data-table.js
, or whatever name you wish, with the following content:
importVueDataTablefrom'@uwlajs/vue-data-table'import'@uwlajs/vue-data-table/dist/style.css'exportdefaultdefineNuxtPlugin(nuxtApp=>{nuxtApp.vueApp.use(VueDataTable)})
Nuxt automatically loads the files in theplugins/
directory by default.
This plugin integrates with Laravel's pagination API, so it fetches dataasynchronously from the provided URL. Follow the instrunctions in theasync data section for a detailed setup.
Onlycolumns
are required. Other props are optional.
Ifdata
is not passed, thenfetchUrl
andfetchCallback
must be passed.
vKey
is not required but ishighly recommend to set it if you plan toadd 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 ifcolumnKeys is set |
columnKeys | Array | - | Array of strings matching the object keys indata . Discarded ifcolumns 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 ofperPageSizes |
fetchUrl | String | - | The URL to fetch data from ifdata is null |
fetchCallback | String | - | Async function which takes an URL and returnsdata matching Laravel's pagination API |
isLoading | Bool | false | Whether table data is loading. Table rows are shown only if this value is set tofalse |
loadingComponent | String ,Object | - | VueJS component to be shown ifisLoading is set totrue |
showPerPage | Bool | true | Whether to show thePerPage component |
showEntriesInfo | Bool | true | Whether to show theEntriesInfo component |
showSearchFilter | Bool | true | Whether to show theSearchFilter component |
showPagination | Bool | true | Whether to show thePagination 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 HTMLclass 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 | - | Thev-key , the key indata 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 ofkey . 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 |
Ifcolumns
is not defined, thencolumnKeys
must be defined and it will bemapped to acolumns
array with the default parameters. Example:
// we can define the columnsconfig={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 arrayreturna.permissions.length-b.permissions.length;},/* custom function to allow searching the permission array */searchFunction:function(search,data){returndata.permissions.some(permission=>permission.includes(search))},searchable:true,/* custom component to display the permissions */component:UserPermissionList,}]}// or use columnKeys shortcutconfig={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 adata
property to receive the data of the currentrow for the component to display it.
In the previous code snippet, we used our custom componentUserPermissionList
.Below is a sample of that custom component.
<template> <div> List of permissions for the user {{ data.name }} : <ul> <liv-for="(permission, i) in data.permissions":key="i"> {{ permission }} </li> </ul> </div></template><script>exportdefault { name:"UserPermissionList", props: { data: { type:Object, required:true } }}</script>
To handle events triggered by a custom component (such as clicking a button in acomponent), the component should emit an event calleduserEvent
and pass anarbitrary payload to it. The event will be propagated upwards byVueDataTable
,which will also emit an event calleduserEvent
whose payload is the same asthe one emitted by the custom component. For example:
<template> <inputtype="checkbox"class="form-control":checked="value"@change='toggleChecked' /></template><script>export {name: 'CheckboxCell',data() {return {value:false, } }, methods: {toggleChecked() {constpayload= { id:this.data.id, checked:this.value, }this.$emit('userEvent', payload) } }, props: { data:Object, }}</script>
When the users clicks the checkbox, it will emit anuserEvent
event, which canbe accessed from theVueDataTable
. Here is an continuation of the previousexample.
<template> <divclass="dashboard"> <h1>DASHBOARD</h1> <buttonclass="btn btn-danger"> DELETE SELECTED ROWS </button> <vue-data-table:data="data":columns="columns"@userEvent="handleEvent" /> </div></template><script>exportdefault {data() {return { data: [/**/], columns: [/**/], selectedRows: [], } }, methods: {handleEvent(payload) {const {checked,id }= payloadif (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 thecustom componentCheckboxCell
, it will emit an event that is handled by themethodhandleEvent
. This method will add/remove theid
of the row to/fromtheselectedRows
array. When the user clicks the "dangerous delete button", itwill deleted the selected rows from the table (on the client side only).
VueDataTable
provides a component calledVdtActionButtons
, which can be usedto 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-tablev-bind="params"@userEvent="handleUserEvent"/> </main></template><script>import {VdtActionButtons }from'@uwlajs/vue-data-table'exportdefault {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-tablev-bind="params"@userEvent="handleUserEvent"/> </main></template><script>import {VdtActionButtons }from'@uwlajs/vue-data-table'exportdefault {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 whosepayload is an object with two fields:action
anddata
. Theaction
is thename of the action (view, edit, delete) anddata
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 settingseditable
to true in thecolumn definition.
columns:{['key':name,editable:true],['key':email,editable:true],['key':phone,editable:true],// ...}
This will makeVueDataTable
display anedit
button on the right side of thecell's text. When the user clicks the button, it will show an input, so the usercan enter a new value for the cell. The user can cancel the editing or confirm.If the user confirms editing,VueDataTable
will emit auserEvent
whosepayload looks like the following:
{action:'updateCell',key:'<key String>',data:'<data Object>',value:'<value String>',}
Wherekey
is the key of the column (if user edits thename
column, thekey
will bename
), thedata
is the object of the row which was edit (an example:{ id: 100, name: 'joe', email: 'joe@email.test' }
), andvalue
is the valueinserted by the user (such asJoe 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 thedata
array on the clientside. 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>exportdefault {/* ...*/ methods: {handleUserEvent(payload) {if (payload.action==='updateCell') {// send some ajax request// ...// then update the cellthis.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)returnlet newRow= {...this.data[ind]} newRow[field]= valuethis.data.splice(ind,1, newRow) }, }}</script>
VueDataTable
provides the built-invdt-cell-selectable
component to selecttable rows.
constprops={ columns=[{title:"",component:"vdt-cell-selectable"// <-- ADD THIS},{key:"name"},{key:"email"},/* ... */], vKey="id",};constdata=[{id:1,name:"joe",email:"joe@example.com"},/* ... */]
When the user toggles the checkbox,VueDataTable
emits an event calleduserEvent
with the following payload:
{action:"select",selected:true||false,// this is the current value of the checkboxdata:{},// this is the current row (example, a user from an users array)}
You can have a reactive variable to keep track of selected items:
constselected=ref([]);consthandleSelect(payload){constitem=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 ormass edition.
Currently,VueDataTable
has support for the following languages: English (en),Brazilian Portuguese (pt-br), and Spanish(es). Thelang
prop specifies inwhich language to display the text in our table.
If we want to add a custom text (maybe because there is no language support orbecause we want something else), we have to set it in thetext
prop.
The following table shows the texts we can customize and their default valuesfor 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 inanyVueDataTable
instance as follow:
import{languageServiceProvider}from"@uwlajs/vue-data-table";constloremIpsumLanguage={perPageText:"lorem ipsum",nextButtonText:"labore ipsum",/* more ... */};languageServiceProvider.setLang("lorem",loremIpsumLanguage)
You can also change any default text for an existing language and that willreflect 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 toVueDataTable
viathedata
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 responsefollowing Laravel's pagination API.
Here is a samplefetchCallback
:
<template> <h1>Users</h1> <vue-data-tablev-bind="vdtProps" /></template><script setup>constvdtProps= { 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-infetch
, but you can also useaxios
or Nuxt's$fetch
under the hood. Just make sure the response returnedby the callback matches the following.
The response from thefetchCallback
should look like this:
{"data": [ {"id":1,"name":"Miss Juliet Heidenreich","email":"alvera13@example.org"}, {"id":2,"name":"Heloise Boehm","email":"joany.feil@example.net"}, {"id":3,"name":"Antwon Collins","email":"xhills@example.com}, /* ... */ ],"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:
<?phpuseApp\Models\User;useIlluminate\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 tosort andsearch using endpoints compatible withLaravel's API, this plugin provides support for Spatie's Laravel Query Builderpackage, which allows you to easily generate API endpoints with sorting andsearching functionalities with well-defined standard.
<?phpuseApp\Models\User;useIlluminate\Support\Facades\Route;useSpatie\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 donot need to worry about the URLs if you are using Spatie's Laravel Query Bulder,becauseVueDataTable
follows their endpoint standard and automatically generates the urls.
If you do not use their package, then you should parse theurl
variable insidethefetchCallback
, 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 allfilters. That means if you have&filter[name]=Ana&filter[email]=Ana
, thenyou will only get results that bothname
andemail
fields match Ana. Ifname
matches Ana but not theemail
column, then this row would not appear.
Here is how you can implementOR
logic using their package:
<?php// routes/app.phpuseApp\Http\Filters\FilterOrWhere;useApp\Models\User;useIlluminate\Support\Facades\Route;useSpatie\QueryBuilder\QueryBuilder;useSpatie\QueryBuilder\AllowedFilter;Route::get('users',function () {return QueryBuilder::for (User::class) ->allowedSorts(['name','email']) ->allowedFilters([ AllowedFilter::custom('name',newFilterOrWhere), AllowedFilter::custom('email',newFilterOrWhere) ]) ->paginate();});// app/Http/Filters/FilterOrWhere.phpnamespaceApp\Http\Filters;useSpatie\QueryBuilder\Filters\Filter;useIlluminate\Database\Eloquent\Builder;class FilterOrWhereimplements Filter{publicfunction__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/SCSSand overriding the defaults.
By default, this is howVueDataTable
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 thecomponents as you want.
Besides a custom component for each column, you provide custom components forthe table's footer, the column'ssorting icon
(the icon displayed if thecolumns is sorted), and the column'ssorting index
(the index of the currentcolumn if it is being sorted and multi column sorting is enabled).
The propertyfooterComponent
sets the component to render the table's footer.The component can be either the componentObject
, or aString
equals to thename of the registered component.
ThefooterComponent
must be a<tfoot>
HTML element and it must have thepropertiesdata
,dataDisplayed
,dataFiltered
. If the component does notspecify those properties inprops
, Vue will probably think they are somecustom HTML attribute and their values will be show as HTML attributes, which isreally messy.
The propertydata
correspond to all data passed toVueDataTable
. ThedataDisplayed
corresponds to all data that is currently visible on the table.ThedataFiltered
corresponds to all data that was filtered by a search query.These properties can be used to perform common operations such as calculatingthe sum of the values of the total rows of a certain column.
Suppose we have a table that of fruits. Thedata
is an array of objects whoseproperties arename
,price
, andamount
. We can provide a custom footer toshow the total amount of fruits bought and the total price.
The footer component would be something like:
<template> <tfootv-show="dataDisplayed.length > 0"> <td>Total</td> <td></td> <td>{{ totalAmount }}</td> <td>{{ totalPrice }}</td> </tfoot></template><script>exportdefault { name:"TableFooter", computed: {totalPrice() {let s=0;for (let fofthis.dataDisplayed) s+=f.price*f.amount;return s; },totalAmount() {let s=0;for (let fofthis.dataDisplayed) s+=f.amount;return s; } }, props: { data:Array, dataDisplayed:Array, dataFiltered:Array, }}</script>
And we pass this component as follow:
<template> <data-tablev-bind="tableProps"/></template><script>importTableFooterfrom'./TableFooter.vue'exportdefault {/* ... 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 */importTableFooterfrom'./TableFooter.vue'Vue.component("table-footer",TableFooter)/* later on */ footerComponent:"table-footer"
By default,VueDataTable
will display arrows to indicate the sorting directionwhen sorting a column. TheSortingIcon
component is wrapped in ath
element.Theth
element has adata-sorting
attribute that may beasc
ordesc
only. Based on this value, we display anarrow_up
or anarrow_down
iconusingCSS
rules.
<template> <spanclass="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:
importSortingIconfrom"./path/to/SortIcon.vue";exportdefault{computed:{bindings(){return{SortingIconComponent:SortingIcon,data:[],/**/};}}}
When sorting multiple columns,VueDataTable
will display an icon with a indexindicating which column has the priority in the sorting process.
<template> <spanclass="data-table-sort-index"> {{ index }} </span></template>
If we want to add our own component for this, we can register it just like wedid before.
importSortingIndexfrom"./path/to/SortingIndex.vue";exportdefault{computed:{bindings(){return{SortingIndexComponent:SortingIndex,data:[],/**/};}}};
In ourSortingIndex
component, we must have aindex
property, whichcorrespondent to the index of the column in the sorting process.
exportdefault{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.
About
Vue data table plugin
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.