gorlabs/tailwind-datatables
Laravel package that integrates Yajra DataTables with a Tailwind CSS + Alpine.js UI. Supports server-side processing, search, pagination, sorting, buttons, and rich column customization. Publish config and optional CSS assets for easy theming.
This guide provides step-by-step instructions for creating a Laravel project, setting up Vue and Inertia.js, and installing and configuring the gorlabs/tailwind-datatables package.
First, create a new Laravel project. Please start your project development with this step; it's crucial for everything to begin with a correct setup:
laravel new gorlabs-datatable-vue && cd gorlabs-datatable-vue && composer require laravel/breeze --dev && touch resources/js/bootstrap.js && php artisan breeze:install vue && rm resources/js/app.ts && rm vite.config.ts && rm -rf resources/js/pages
Fixing package.json Clutter
The package.json file that comes with a fresh Laravel-Vue stack installation is clean and organized. However, when you run the breeze:install vue command, this scaffolding unfortunately breaks the existing structure. Our installation script already mitigates most of the damage caused by this addition, but now you need to clean up your package.json file because it has been cluttered by the breeze:install vue command and will cause conflicts.
At this stage, your package.json file should look like this:
{
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
"dev": "vite",
"format": "prettier --write resources/",
"format:check": "prettier --check resources/",
"lint": "eslint . --fix"
},
"devDependencies": {
"[@eslint](https://github.com/eslint)/js": "^9.19.0",
"[@tailwindcss](https://github.com/tailwindcss)/forms": "^0.5.3",
"[@types](https://github.com/types)/node": "^22.13.5",
"[@vue](https://github.com/vue)/eslint-config-typescript": "^14.3.0",
"autoprefixer": "^10.4.12",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"postcss": "^8.4.31",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.23.0",
"vue-tsc": "^2.2.4"
},
"dependencies": {
"[@inertiajs](https://github.com/inertiajs)/vue3": "^2.0.0",
"[@tailwindcss](https://github.com/tailwindcss)/vite": "^4.1.1",
"[@vitejs](https://github.com/vitejs)/plugin-vue": "^5.2.1",
"[@vueuse](https://github.com/vueuse)/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.0",
"lucide-vue-next": "^0.468.0",
"reka-ui": "^2.2.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
"typescript": "^5.2.2",
"vite": "^6.2.0",
"vue": "^3.5.13",
"ziggy-js": "^2.4.2"
}
}
composer require gorlabs/tailwind-datatables
Publish the package assets:
composer update
php artisan vendor:publish --tag=tailwind-datatables-views
php artisan vendor:publish --tag=gorlabs-tailwind-datatables-config
php artisan vendor:publish --tag=tailwind-datatables-css
Next, update the content of the resources/js/app.js file as shown below. This file should already exist.
// resources/js/app.js
import '../css/app.css';
// ********************************************************************
// * DataTables ve İlgili Kütüphaneler (SADECE GLOBAL İHTİYAÇLAR) *
// ********************************************************************
import jQuery from 'jquery';
window.jQuery = jQuery;
window.$ = jQuery;
import 'datatables.net';
import 'datatables.net-buttons';
import 'datatables.net-buttons/js/buttons.html5.js';
import 'datatables.net-buttons/js/buttons.print.js';
import 'datatables.net-buttons/js/buttons.colVis.js';
import 'datatables.net-responsive';
import JSZip from 'jszip';
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';
window.JSZip = JSZip;
window.pdfMake = pdfMake;
window.pdfMake.vfs = pdfFonts.vfs;
import Swal from 'sweetalert2';
window.Swal = Swal;
import dayjs from 'dayjs';
window.dayjs = dayjs;
import Alpine from 'alpinejs';
import { registerGorlabsDatatablesAlpineComponents } from '../../vendor/gorlabs/tailwind-datatables/resources/js/crud-datatable';
// ********************************************************************
// * Mevcut Inertia.js ve Vue başlatma kodları (DEĞİŞTİRME) *
// ********************************************************************
import { createInertiaApp } from '[@inertiajs](https://github.com/inertiajs)/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createApp, h } from 'vue';
import { ZiggyVue } from 'ziggy-js';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue'),
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
// New Section to Add: Wait for Alpine.js to load and register components
// This event listener fires when Alpine.js is loaded from CDN and ready.
registerGorlabsDatatablesAlpineComponents(Alpine); // Bileşenleri kaydet
Alpine.start();
Update postcss.config.js content
export default {
plugins: {
'[@tailwindcss](https://github.com/tailwindcss)/postcss': {},
autoprefixer: {},
},
};
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '[@vitejs](https://github.com/vitejs)/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js'
],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
build: {
minify: true, // Minificaiton open
sourcemap: true, // Hata ayıklama için sourcemap oluştur
},
server: {
cors: true,
},
});
//resources/js/types/globals.d.ts
import { AppPageProps } from '@/types/index';
import type Swal from 'sweetalert2'; // type import kullan
import type dayjs from 'dayjs'; // type import kullan
import type Alpine from 'alpinejs'; // type import kullan
import type * as pdfMake from 'pdfmake/build/pdfmake'; // type import kullan
import type * as JSZip from 'jszip'; // type import kullan
// Extend ImportMeta interface for Vite...
declare module 'vite/client' {
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
[key: string]: string | boolean | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
readonly glob: <T>(pattern: string) => Record<string, () => Promise<T>>;
}
}
declare module '[@inertiajs](https://github.com/inertiajs)/core' {
interface PageProps extends InertiaPageProps, AppPageProps {}
}
declare module 'vue' {
interface ComponentCustomProperties {
$inertia: typeof Router;
$page: Page;
$headManager: ReturnType<typeof createHeadManager>;
}
}
declare global {
interface Window {
GorlabsDatatables: {
date: (format: string) => (data: any, type: string, row: any) => string;
statusBadge: (publishedText: string, draftText: string) => (data: any, type: string, row: any) => string;
actions: (updateUrlPrefix: string, deleteUrlPrefix: string) => (data: any, type: string, row: any) => string;
// Diğer GorlabsDatatables metotlarını da buraya ekleyin
};
Swal: any; // SweetAlert2 için de benzer şekilde ekleyebilirsiniz
openFormModal: (title: string, url: string, item: any | null) => void; // Global fonksiyon için de
}
}
// ********************************************************************
// * Yeni Eklenecek Kısım: Global Window Objektif Tanımları *
// ********************************************************************
declare global {
interface Window {
Swal: typeof Swal;
dayjs: typeof dayjs;
Alpine: typeof Alpine;
pdfMake: typeof pdfMake;
JSZip: typeof JSZip;
jQuery: JQuery;
$: JQuery;
crudDataTable: (config: any) => any;
}
}
php artisan make:migration create_posts_table
The generated file path will be: database/migrations/YYYY_MM_DD_HHMMSS_create_posts_table.php (where YYYY_MM_DD_HHMMSS will vary based on the date and time). Update its content as follows:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamp('published_at')->nullable(); // Yayım tarihi
$table->boolean('is_published')->default(false); // Durum (yayınlandı mı?)
$table->string('status')->default('draft'); // Örnek olarak eklendi
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Create the Post model and PostFactory.php and replace their contents with the following:
php artisan make:model Post -f
The generated file path will be: app/Models/Post.php. Update its content as follows:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content',
'published_at',
'is_published',
'status',
];
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
];
}
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* [@extends](https://github.com/extends) \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
*/
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* [@return](https://github.com/return) array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->sentence(rand(3, 8)),
'content' => fake()->paragraphs(rand(1, 3), true),
'published_at' => fake()->optional(0.8)->dateTimeThisYear(), // %80 ihtimalle bir tarih verir
'is_published' => fake()->boolean(90), // %90 ihtimalle true
'status' => fake()->randomElement(['draft', 'published', 'archived']),
];
}
}
php artisan make:seeder PostSeeder
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// 50 adet sahte Post oluştur
Post::factory()->count(50)->create();
}
}
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Database\Factories\PostFactory;use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->call([PostSeeder::class,]);
}
}
Create the PostsDataTable class and replace its content with the following:
php artisan datatable:make PostsDataTable --model=Post
The generated file path will be: app/DataTables/PostsDataTable.php. Update its content as follows:
<?php
namespace App\DataTables;
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\Builder as HtmlBuilder;
use Yajra\DataTables\Html\Button;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
class PostsDataTable extends DataTable
{
public function query(Post $model): QueryBuilder
{
return $model->newQuery()->select('id', 'title', 'content', 'is_published', 'published_at', 'status', 'created_at', 'updated_at');
}
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->setRowId('id')
->addColumn('select_checkbox', function(Post $post) {
return '';
})
->addColumn('actions', function(Post $post) {
return '';
})
->editColumn('is_published', function(Post $post) {
return $post->is_published ? 1 : 0;
})
->editColumn('published_at', function(Post $post) {
return $post->published_at ? $post->published_at->format('Y-m-d H:i:s') : null;
})
->editColumn('title', function(Post $post) {
$title = $post->title;
if (strlen($title) > 24) {
return substr($title, 0, 24) . ' ...';
}
return $title;
})
->editColumn('content', function(Post $post) {
$content = $post->content;
if (strlen($content) > 34) {
return substr($content, 0, 34) . ' ...';
}
return $content;
});
}
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('posts-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(1)
->select(false)
->buttons([
Button::make('create'),
Button::make('export'),
Button::make('print'),
Button::make('reset'),
Button::make('reload')
]);
}
public function getColumns(): array
{
$columns = [
Column::computed('select_checkbox')
->title('<input type="checkbox" id="select-all-checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">')
->exportable(false)
->printable(false)
->width(10)
->addClass('text-center')
->orderable(false)
->searchable(false)
->footer('')
->responsivePriority(1),
Column::make('id')->responsivePriority(2),
Column::make('title')->responsivePriority(3),
Column::make('content')->responsivePriority(5),
Column::make('published_at')
->title('Published At')
->width(150)
->responsivePriority(4),
Column::make('is_published')
->title('Published')
->width(100)
->responsivePriority(4),
Column::make('status')->responsivePriority(4),
];
$columns[] = Column::computed('actions')
->title('ACTIONS')
->exportable(false)
->printable(false)
->width(120)
->addClass('text-center')
->orderable(false)
->searchable(false)
->footer('')
->responsivePriority(1);
return $columns;
}
protected function filename(): string
{
return 'Posts_' . date('YmdHis');
}
}
Create the PostController class and replace its content with the following:
php artisan make:controller PostController
The generated file path will be: app/Http/Controllers/PostController.php. Update its content as follows:
<?php
namespace App\Http\Controllers;
use App\DataTables\PostsDataTable;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class PostController extends Controller
{
public function index(PostsDataTable $postsDataTable)
{
$columns = $postsDataTable->getColumns();
$formattedColumns = collect($columns)->map(function ($column) {
return [
'data' => $column->data,
'name' => $column->name,
'title' => $column->title,
'orderable' => $column->orderable,
'searchable' => $column->searchable,
'className' => $column->className ?? '',
];
})->toArray();
return Inertia::render('Posts/Index', [
'columns' => $formattedColumns,
]);
}
public function ajaxData(PostsDataTable $dataTable)
{
Log::info('DataTables AJAX isteği parametreleri:', request()->all());
return $dataTable->dataTable($dataTable->query(new Post()))->make(true);
}
public function create()
{
return view('tailwind-datatables::datatables.form', ['post' => new Post()]);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_published' => 'nullable|boolean',
'published_at' => 'nullable|date',
]);
$data = $request->except(['published_at']);
$data['published_at'] = $request->input('published_at') ? now()->parse($request->input('published_at')) : null;
$data['is_published'] = (bool) $request->input('is_published', 0);
$post = Post::create($data);
return response()->json(['success' => 'Post başarıyla oluşturuldu.', 'post' => $post]);
}
public function edit(Post $post)
{
$post->published_at_formatted = $post->published_at ? $post->published_at->format('Y-m-d\TH:i') : '';
return view('tailwind-datatables::datatables.form', compact('post'));
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_published' => 'boolean|nullable',
'published_at' => 'nullable|date',
]);
$data = $request->except(['published_at']);
$data['is_published'] = (bool) $request->input('is_published', 0);
$data['published_at'] = $request->input('published_at') ? now()->parse($request->input('published_at')) : null;
$post->update($data);
return response()->json(['success' => 'Post başarıyla güncellendi.', 'post' => $post]);
}
public function destroy(Post $post)
{
try {
$post->delete();
return response()->json(['success' => 'Post başarıyla silindi.']);
} catch (\Exception $e) {
Log::error('Post silinirken hata oluştu: ' . $e->getMessage(), ['post_id' => $post->id]);
return response()->json(['error' => 'Post silinirken bir hata oluştu.'], 500);
}
}
}
Create the resources/js/Pages/Posts/Index.vue file and replace its content with the following:
mkdir -p resources/js/Pages/Posts
touch resources/js/Pages/Posts/Index.vue
The generated file path will be: resources/js/Pages/Posts/Index.vue. Update its content as follows:
Kod snippet'i
<script setup lang="ts">
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '[@inertiajs](https://github.com/inertiajs)/vue3';
import { onMounted, ref } from 'vue';
// Backend'den gelen columns prop'unu tanımla
const props = defineProps<{
columns: Array<{
data: string;
name: string;
title: string;
orderable: boolean;
searchable: boolean;
className?: string; // The className option can be optional
}>;
}>();
console.log('Props Columns:', props.columns);
// Create a ref for DataTables' HTML structure
const dataTableContainer = ref<HTMLElement | null>(null);
// We're defining the crudDataTable config using the columns from props.
const datatableConfig = {
datatableId: 'posts-table',
ajaxUrl: route('posts.data'),
columns: props.columns, // We're getting the columns from props!
perPage: 10,
perPageSelect: [10, 25, 50, 100, -1],
addNewButtonText: 'Add New Post',
addEditUrl: route('posts.create'),
updateUrlPrefix: route('posts.update', ''),
deleteUrlPrefix: route('posts.destroy', ''),
initialFormState: {},
responsive: true, // The responsive feature is set to true here
};
// A ref to store the DataTables instance
const dataTableInstance = ref<any>(null);
const $ = window.$ as any;...
How can I help you explore Laravel packages today?