Build powerful data-driven interfaces with Laravel, Vue, and DataTables
Everything that I’ve seen so far on how to setup DataTables did not yield the kind of results that I needed. I wanted a simple integration of DataTables into my Vue.js/Laravel project, but that was proving more difficult to find than not. This shouldn’t be as difficult as it was made out to be. Let’s take an extremely simple approach to this.
Read the previous article on setting up Vue and Vue-Router into the Laravel project.
Recap
We have a route that directs users to index.blade.php
via a route in web.php
.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/{all}', function () {
return view('index');
})->where("all", ".*");
The index.blade.php file looks like this as of right now.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Styles -->
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.text-center{text-align:center}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}}
</style>
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
@vite(['resources/js/app.js', 'resources/css/app.css'])
</head>
<body class="antialiased">
<div id="app"></div>
<script src="https://unpkg.com/flowbite@1.4.1/dist/flowbite.js"></script>
</body>
</html>
#app
. We also made a modification to the resources/js/app.js
file. We mount
a vue file to the #app
and also use the router
.import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
createApp(App)
.use(router)
.mount("#app");
The vue file that we mount
is called resources/js/App.vue
, which has the following code.
<template>
<Header />
<router-view />
<Footer />
</template>
<script>
import Header from './components/header/Header.vue';
import Footer from "./components/footer/Footer.vue";
export default {
name: 'App',
components: {
Footer,
Header,
},
}
</script>
<style>
</style>
It contains a Header
, followed by the router-view
, and finally the Footer
. The resources/js/router/index.js
router file looks like this:
import {createRouter, createWebHistory } from "vue-router";
import Home from "../../views/home/Index.vue";
import About from "../../views/about/Index.vue";
const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/about",
name: "about",
component: About,
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
That’s all there is to it. We have a functioning Vue/Laravel installation. To view all of the dependencies necessary to make this project run so far, make sure to read my article on Laravel with Vue and Vue-Router.
resources/views/index.blade.php
We need to add the CDN for DataTables. DataTables depends on jQuery, so we’ll need to add it as well. In the head
tag, we’ll first add the stylesheet.
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.1/css/jquery.dataTables.css">
Before the closing body
tag, we’ll add the jQuery
and DataTables
scripts.
<script src="https://code.jquery.com/jquery-3.6.2.min.js" integrity="sha256-2krYZKh//PcchRtd+H+VyyQoZ/e3EcrkxhM8ycwASPA=" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.13.1/js/jquery.dataTables.js"></script>
You can grab the latest minified version on jQuery from here.
The full code should now look like this for index.blade.php
.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Styles -->
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.text-center{text-align:center}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}}
</style>
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.1/css/jquery.dataTables.css">
@vite(['resources/js/app.js', 'resources/css/app.css'])
</head>
<body class="antialiased">
<div id="app"></div>
<script src="https://unpkg.com/flowbite@1.4.1/dist/flowbite.js"></script>
<script src="https://code.jquery.com/jquery-3.6.2.min.js" integrity="sha256-2krYZKh//PcchRtd+H+VyyQoZ/e3EcrkxhM8ycwASPA=" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.13.1/js/jquery.dataTables.js"></script>
</body>
</html>
Seeding Users Table
If you haven’t run your migrations yet, make sure to do so: php artisan migrate
. If you need User records, open database/seeders/DatabaseSeeder.php
and uncomment the User
factory.
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
\App\Models\User::factory(10)->create();
// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
}
}
Seed your users
table as many times as you’d like: php artisan db:seed
. You may want to change the number of records that you would like to create by modifying the \App\Models\User::factory(10)->create()
to \App\Models\User::factory(1000)->create()
or whatever number you’d like.
resources/views/user/Index.vue
It’s time to create a vue file. I normally like to keep my views in the views directory. We’ll use our standard template and start populating it. The first step is to add your table
inside of the template
.
<table id="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
We’re going to list out the user’s id, name, and email address. We now need to bind everything to this table. That’ll be done in the script
tag.
<script>
export default {
name: 'User',
created() {
jQuery(document).ready( function ($) {
$.noConflict();
$('#table').DataTable({
"processing": true,
"serverSide": true,
"ajax": "api/users",
"columns": [
{ "data": "id" },
{ "data": "name" },
{ "data": "email" },
]
});
});
}
}
</script>
As you can see, we’ll use ajax
to call our api/users
route. We haven’t done that yet, but we will shortly. The full resources/views/user/Index.vue
file now looks like this.
<template>
<div>
<table id="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'User',
created() {
jQuery(document).ready( function ($) {
$.noConflict();
$('#table').DataTable({
"processing": true,
"serverSide": true,
"ajax": "api/users",
"columns": [
{ "data": "id" },
{ "data": "name" },
{ "data": "email" },
]
});
});
}
}
</script>
<style>
</style>
Installing the DataTables Laravel Dependency
Before we actually start returning anything from our route, we’ll need to install a really good DataTables dependency.
To install it, run:
composer require yajra/laravel-datatables-oracle:"^10.0"
This is located on the documentation. Make sure to run your dev environment again: npm run dev
.
routes/api.php
Time to add our route to the api.php
file. We’ll use the datatables
global function now that we have it installed.
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// ...
Route::get('/users', function() {
$users = \App\Models\User::select('id', 'name', 'email');
return datatables($users)->make(true);
});
After we generate the query that we want to pass to datatables
, we’ll pass it as an argument. Make sure that you’re not actually retrieving the content yourself; in other words, don’t add ->get()
at the end of your statement.
resources/js/router/index.js
We finally need to modify our router so that it knows how to process the users route.
import {createRouter, createWebHistory } from "vue-router";
import Home from "../../views/home/Index.vue";
import About from "../../views/about/Index.vue";
import User from '../../views/users/Index.vue';
const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/about",
name: "about",
component: About,
},
{
path: "/users",
name: "users",
component: User,
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
And that’s it. Take it for a spin.
There is one final thing that we should do just to make this article complete and that is to add a column that’s not part of the query. Most times, you want an Edit
or Delete
button in the table. Let’s see how we can add that.
Adding a Button to the Table
Open your resources/views/users/Index.vue
file. Let’s add a new column to our table and also to our script. I’m going to name it Action
and will also add the { “data”: “action” }
object to our columns
property.
<template>
<div>
<table id="myTable">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Email</th>
<th>Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'User',
created() {
jQuery(document).ready( function ($) {
$.noConflict();
$('#myTable').DataTable({
"processing": true,
"serverSide": true,
"ajax": "api/users",
"columns": [
{ "data": "id" },
{ "data": "name" },
{ "data": "email" },
{ "data": "action" },
]
});
});
}
}
</script>
<style>
</style>
We need to modify the API route to return a column.
Route::get('/users', function() {
$users = \App\Models\User::select('id', 'name', 'email');
return datatables($users)
->addColumn('action', function($row) {
return '<a href="/users/' . $row['id'] . '/edit">Edit</a>';
})
->rawColumns(['action'])
->make(true);
});
Refresh your page and you should see something like this:
Now we can say that we’re done. See you next time.
Laravel Series
Continue your Laravel Learning.
Build powerful data-driven interfaces with Laravel, Vue, and DataTables
Laravel 9.x with Vue.js and DataTables
In this comprehensive guide, explore how to integrate Laravel 9.x with Vue.js and DataTables for streamlined data management. Learn how to handle large datasets, create engaging user interfaces, and boost performance using advanced filtering, pagination, and real-time updates.