Previously I’ve used Datatables.net to build my tables on the Laravel blade templates for user interface. Now I’ve been migrating over to Vue.js I thought I’d look at another option – more Vue.js centric.

This is where vuetable-2 (not to be confused with other vue components of a similar name!) came in. I tried a lot of other vue tables and most of them had a dependency for jQuery. I really wanted one that didn’t use jQuery – I know Bootstrap has it as a dependency, but if I ever decide to remodel the UX using something like Material Design, I’d have no need to include jQuery.

As a component I needed to do a lot of work to configure it to work with my setup. But only really because I wanted it to work with Bootstrap 4 and fontawesome. So I had to build vue component templates that presented me with the correct structure and classes for the pagination and search.

The examples in the Gists relate to my admin interface to manage a Laravel user model of users and roles.

(see gists below)

JWTAuth

Then I ran into a problem with fetching data. Vuetable default Axios call to retrieve data to populate the table meant that no Authorization header was being sent. The default property for the vuetable api-url uses Axios, but obviously a different instance to the Vue plugin, as it doesn’t include the @websnova/vue-auth component.

The resolution was simple. Add a property to use your own http-fetch function, but just use the defaults, no need to add anything as effectively using this.http$ uses the Vue plugin Axios instance that includes the @wesanova parts.

api-url="myApi"
:http-fetch="getData"

Then add a matching method, but do nothing special:

 getData(apiUrl, httpOptions) {
   return this.$http.get(apiUrl, httpOptions)
},

Gists

<template>
<div id="users">
<div class="row">
<vuetable-filter-bar
class="col-sm-6"
/>
<! Api call should return standard Laravel paginate() json >
<vuetable-pagination
ref="pagination"
class="col-sm-6"
@vuetable-pagination:change-page="onChangePage"
/>
</div>
<! Generate the table based on the default api call >
<vuetable
ref="vuetable"
:fields="fields"
:css="css"
:sort-order="sortOrder"
:append-params="moreParams"
:http-fetch="getUsers"
class="table-striped table-hover"
api-url="/admin/users"
pagination-path=""
@vuetable:pagination-data="onPaginationData"
>
<template
slot="roles"
slot-scope="props"
>
<! Iterate the roles and build the buttons >
<button
v-for="role in props.rowData.roles"
:key="role.name"
class="btn btn-sm btn-primary mr-1 mb-1"
>
{{ role.name }}<!&nbsp;<i class="fas fa-times"/>>
</button>
<! add role button fires the modal >
<button
class="btn btn-sm btn-default text-dark mr-1 mb-1"
@click="addRoles(props.rowData)"
>
<i class="fas fa-pencil-alt"/>
</button>
</template>
</vuetable>
<div class="row">
<! Api call should return standard Laravel paginate() json >
<vuetable-pagination-dropdown
ref="paginationDropdown"
class="offset-sm-6 col-sm-6"
@vuetable-pagination:change-page="onChangePage"
/>
</div>
<! Modal for the updating the current users roles >
<div
id="rolesModal"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog modal-dialog-centered"
role="document"
>
<div class="modal-content">
<form
name="rolesForm"
action=""
method="put"
@submit.prevent="saveRoles"
>
<input
type="hidden"
name="_method"
value="put"
>
<input
:value="user.username"
name="user"
type="hidden"
>
<div class="modal-header">
<h5 class="modal-title">Roles for User "{{ user.name }}"</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div
v-for="role in roles"
:key="role.name"
class="col-sm-4"
>
<!
Checkboxes are bound to a non-indexed array where the id
is the value of the array item
>
<div class="form-group">
<label :for="role.name">
<input
:id="role.name"
:value="role.name"
v-model="newRoles"
type="checkbox"
>
{{ role.name }}
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
data-dismiss="modal"
>
Close&nbsp;<i class="fas fa-times"/>
</button>
<button
type="button"
class="btn btn-danger"
@click="newRoles = []"
>
None&nbsp;<i class="fas fa-trash-alt"/>
</button>
<button
type="submit"
class="btn btn-primary"
>
Save&nbsp;<i class="fas fa-save"/>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import Vuetable from 'vuetable-2'
// Bootstrap 4 style pagination
import VuetablePagination from '../Vuetable/VuetablePaginationBootstrap'
import VuetablePaginationDropdown from '../Vuetable//VuetablePaginationDropdownBootstrap'
import VuetableCss from '../Vuetable/VuetableCss'
import fieldsDef from './usersFieldsDef'
import VuetableFilterBar from '../Vuetable/VuetableFilterBarBootstrap'
export default {
components: {
Vuetable, // The vuetable
VuetablePagination, // nav/button based pagination
VuetablePaginationDropdown, // select based pagination
VuetableFilterBar // Search
},
data () {
return {
css: VuetableCss, // Reusable Css
fields: fieldsDef, // Field Definitions / rendering
sortOrder: [{
field: 'name',
direction: 'asc'
}],
data: [], // Table data
user: {}, // Current user
roles: {}, // All of the available roles
newRoles: [], // List of roles to be synced when submitted
moreParams: {} // Sends the filter with the other params
}
},
mounted () {
// Listen for the filter events from VuetableFilterBar
this.$events.$on('filter-set', eventData => this.onFilterSet(eventData))
this.$events.$on('filter-reset', e => this.onFilterReset()) // eslint-disable-line no-unused-vars
this.getRoles() // Get all the available roles
},
methods: {
/* Search Filter Handling */
onFilterSet (filterText) {
// console.log('filter-set', filterText)
this.moreParams = {
filter: filterText
}
// console.log(this.$refs.vuetable)
Vue.nextTick( () => this.$refs.vuetable.refresh() ) // eslint-disable-line no-undef
},
onFilterReset () {
// console.log('filter-reset')
this.moreParams = {}
Vue.nextTick( () => this.$refs.vuetable.refresh() ) // eslint-disable-line
},
// Show the modal with the roles of the current user
addRoles(user) {
this.user = user
// Create the non-indexed array "newRoles"
this.newRoles = this.user.roles.map((role) => {
return role.name
})
$('#rolesModal').modal('show') // eslint-disable-line no-undef
},
// Fetch all of the avilable roles that can be used
getRoles: function() {
this.axios.get('/admin/roles') // eslint-disable-line no-undef
.then(({ data }) => {
this.roles = data.data
})
},
// replaces the vuetales own call to api-url to handle jwt auth
getUsers(apiUrl, httpOptions) {
return this.$http.get(apiUrl, httpOptions)
},
// Save the response from the modal form using Laravel resource controller
saveRoles: function() {
axios.put('/admin/users/'+this.user.id, { // eslint-disable-line no-undef
params: {
query: {
user: this.user,
roles: this.newRoles
}
}
}).then(() => {
// Reload the table and hide the modal
this.$refs.vuetable.reload() // Reload keeps the current page number
$('#rolesModal').modal('hide') // eslint-disable-line no-undef
}) // Do we need a catch?
},
// Trigger the pagination changes
onPaginationData (paginationData) {
this.$refs.pagination.setPaginationData(paginationData)
this.$refs.paginationDropdown.setPaginationData(paginationData)
},
onChangePage (page) {
this.$refs.vuetable.changePage(page)
}
}
}
</script>
<style>
/* .vuetable-th-checkbox-id {
width: 2%;
}
.vuetable-th-id {
width: 8%;
} */
.vuetable-th-username {
width: 20%;
}
.vuetable-th-name {
width: 20%;
}
.vuetable-th-email {
width: 15%;
}
.vuetable-th-slot-roles {
width: 65%;
}
.vuetable-pagination nav {
float: right;
}
.vuetable-pagination-dropdown nav {
float: right;
}
</style>

view raw
users.vue .js
hosted with ❤ by GitHub

// Deals with the sort icons using fontawesome
export default {
tableClass: 'table',
loadingClass: 'loading',
ascendingIcon: 'fas fa-arrow-down',
descendingIcon: 'fas fa-arrow-up',
detailRowClass: 'vuetable-detail-row',
handleIcon: 'fas fa-bars',
sortableIcon: '', //fas fa-sort-amount-up', // since v1.7
ascendingClass: 'sorted-asc', // since v1.7
descendingClass: 'sorted-desc' // since v1.7
}

view raw
VueTableCss.js
hosted with ❤ by GitHub

<template>
<div class="vuetable-filter-bar">
<div class="form-inline">
<div class="form-group">
<label class="sr-only">Search for:&nbsp;</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-btn">
<button
class="btn btn-primary"
type="button"
@click="doFilter"
>
<i class="fas fa-search"/>
</button>
</span>
</div>
<input
v-model="filterText"
class="form-control"
placeholder="Search string"
type="text"
@keyup.enter="doFilter"
>
<div class="input-group-append">
<span class="input-group-btn">
<button
class="btn btn-default"
type="button"
@click="resetFilter"
>
<i class="fas fa-times"/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
filterText: ''
}
},
methods: {
doFilter () {
this.$events.fire('filter-set', this.filterText)
},
resetFilter () {
this.filterText = ''
this.$events.fire('filter-reset')
}
}
}
</script>

<template>
<div class="vuetable-pagination">
<nav aria-label="Table Navigation">
<ul class="pagination">
<li
:class="{'disabled': isOnFirstPage}"
class="page-item">
<a
class="page-link"
href="#"
@click.prevent="loadPage(1)">
<span><i class="fas fa-angle-double-left"/></span>
</a>
</li>
<li
:class="{'disabled': isOnFirstPage}"
class="page-item">
<a
class="page-link"
href="#"
@click.prevent="loadPage('prev')">
<span><i class="fas fa-angle-left"/></span>
</a>
</li>
<template v-if="notEnoughPages">
<li
v-for="n in totalPage"
:key="n"
:class="{'active': isCurrentPage(n)}"
class="page-item"
>
<a
class="page-link"
@click.prevent="loadPage(n)"
vhtml="n"/>
</li>
</template>
<template velse>
<li
v-for="n in windowSize"
:key="n"
:class="{'active': isCurrentPage(windowStart+n-1)}"
class="page-item"
>
<a
class="page-link"
@click.prevent="loadPage(windowStart+n1)"
vhtml="windowStart+n-1"/>
</li>
</template>
<li
:class="{'disabled': isOnLastPage}"
class="page-item"
>
<a
class="page-link"
href=""
@click.prevent="loadPage('next')">
<span><i class="fas fa-angle-right"/></span>
</a>
</li>
<li
:class="{'disabled': isOnLastPage}"
class="page-item"
>
<a
class="page-link"
href=""
@click.prevent="loadPage(totalPage)">
<span><i class="fas fa-angle-double-right"/></span>
</a>
</li>
</ul>
</nav>
</div>
</template>
<script>
import { VuetablePaginationMixin } from 'vuetable-2'
export default {
mixins: [VuetablePaginationMixin]
}
</script>

<template>
<div class="vuetable-pagination-dropdown">
<nav aria-label="Table Navigation">
<ul class="pagination">
<li
:class="{'disabled': isOnFirstPage}"
class="page-item">
<a
class="page-link"
href="#"
@click.prevent="loadPage(1)">
<span><i class="fas fa-angle-double-left"/></span>
</a>
</li>
<li
:class="{'disabled': isOnFirstPage}"
class="page-item">
<a
class="page-link"
href="#"
@click.prevent="loadPage('prev')">
<span><i class="fas fa-angle-left"/></span>
</a>
</li>
<div class="form-group mr-1 ml-1">
<select
id="paginationDropdown"
class="form-control"
@change="loadPage($event.target.selectedIndex+1)"
>
<template
v-for="k in totalPage"
>
<option
:key="k"
:value="k"
:selected="isCurrentPage(k)"
>
Page {{ k }}
</option>
</template>
</select>
</div>
<li
:class="{'disabled': isOnLastPage}"
class="page-item"
>
<a
class="page-link"
href=""
@click.prevent="loadPage('next')">
<span><i class="fas fa-angle-right"/></span>
</a>
</li>
<li
:class="{'disabled': isOnLastPage}"
class="page-item"
>
<a
class="page-link"
href=""
@click.prevent="loadPage(totalPage)">
<span><i class="fas fa-angle-double-right"/></span>
</a>
</li>
</ul>
</nav>
</div>
</template>
<script>
import { VuetablePaginationMixin } from 'vuetable-2'
export default {
mixins: [VuetablePaginationMixin]
}
</script>