Building Dynamic Laravel CRUD Applications with AJAX and DataTables Integration


In this article, Iโve explored how to build a fully dynamic CRUD (๐ Create, ๐ Read, โ๏ธ Update, โ Delete) system in Laravel using AJAX โก and the powerful jQuery DataTables ๐ plugin. By combining Laravel's backend robustness ๐ ๏ธ with the responsiveness of AJAX ๐ and the interactive features of DataTables ๐, you'll learn how to create a seamless, real-time user experience ๐ฅ๏ธ for managing data without full page reloads ๐.
walk through a step-by-step implementation ๐ฃ โ from setting up routes ๐งญ and controllers ๐งฉ to writing efficient AJAX calls ๐ and rendering DataTables with server-side processing โ๏ธ. This tutorial is ideal for developers ๐จโ๐ป๐ฉโ๐ป looking to enhance their Laravel applications with modern, responsive, and user-friendly data management capabilities โจ.
Step 1 :
Make migration of a category table:
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('image')->nullable();
$table->enum('status',['active','inactive'])->default('active');
$table->timestamps();
});
}
After that run the migration.
php artisan migrate
Step 2:
Set model :
class Category extends Model
{
protected $fillable = ['name','image','status'];
protected $hidden = ['created_at' , 'image', 'updated_at' , 'status'];
}
Step 3:
Set up controller logic:
use Exception;
use App\Helper\Helper;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use Yajra\DataTables\Facades\DataTables;
class CategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
if ($request->ajax()) {
$data = Category::all();
return DataTables::of($data)
->addIndexColumn()
->addColumn('image', function ($data) {
if ($data->image) {
$url = asset($data->image);
return '<img src="' . $url . '" alt="image" width="50px" height="50px" style="margin-left:20px;">';
} else {
return '<img src="' . asset('default/logo.png') . '" alt="image" width="50px" height="50px" style="margin-left:20px;">';
}
})
->addColumn('status', function ($data) {
$backgroundColor = $data->status == "active" ? '#4CAF50' : '#ccc';
$sliderTranslateX = $data->status == "active" ? '26px' : '2px';
$sliderStyles = "position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background-color: white; border-radius: 50%; transition: transform 0.3s ease; transform: translateX($sliderTranslateX);";
$status = '<div class="form-check form-switch" style="margin-left:40px; position: relative; width: 50px; height: 24px; background-color: ' . $backgroundColor . '; border-radius: 12px; transition: background-color 0.3s ease; cursor: pointer;">';
$status .= '<input onclick="showStatusChangeAlert(' . $data->id . ')" type="checkbox" class="form-check-input" id="customSwitch' . $data->id . '" getAreaid="' . $data->id . '" name="status" style="position: absolute; width: 100%; height: 100%; opacity: 0; z-index: 2; cursor: pointer;">';
$status .= '<span style="' . $sliderStyles . '"></span>';
$status .= '<label for="customSwitch' . $data->id . '" class="form-check-label" style="margin-left: 10px;"></label>';
$status .= '</div>';
return $status;
})
->addColumn('action', function ($data) {
return '<div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
<a href="#" type="button" onclick="goToEdit(' . $data->id . ')" class="btn btn-primary fs-14 text-white delete-icn" title="Delete">
<i class="fe fe-edit"></i>
</a>
<a href="#" type="button" onclick="showDeleteConfirm(' . $data->id . ')" class="btn btn-danger fs-14 text-white delete-icn" title="Delete">
<i class="fe fe-trash"></i>
</a>
</div>';
})
->rawColumns(['status', 'action'])
->make();
}
return view("backend.layouts.category.index");
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('backend.layouts.category.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validate = $request->validate([
'name' => 'required|unique:categories,name',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
]);
try {
if ($request->hasFile('image')) {
$validate['image'] = Helper::uploadImage($request->image, 'category');
}
Category::create($validate);
session()->put('t-success', 'Category created successfully');
} catch (Exception $e) {
session()->put('t-error', $e->getMessage());
}
return redirect()->route('admin.category.index')->with('success', 'Category created successfully');
}
/**
* Display the specified resource.
*/
public function show(Category $category, $id)
{
$category = Category::findOrFail($id);
return view('backend.layouts.category.edit', compact('category'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Category $category, $id)
{
$category = Category::findOrFail($id);
return view('backend.layouts.category.edit', compact('category'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id)
{
$validate = $request->validate([
'name' => 'required',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
]);
try {
$category = Category::findOrFail($id);
if ($request->hasFile('image')) {
if ($category->image && file_exists(public_path($category->image))) {
Helper::deleteImage(public_path($category->image));
}
// $validate['image'] = Helper::uploadImage($request->file('image'), 'category', time() . '_' . Helper::getFileName($request->file('image')));
$validate['image'] = Helper::uploadImage($request->image, 'category');
}
$category->update($validate);
session()->put('t-success', 'Category updated successfully');
} catch (Exception $e) {
session()->put('t-error', $e->getMessage());
}
return redirect()->route('admin.category.index');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
$data = Category::findOrFail($id);
if (empty($data)) {
return response()->json([
'success' => false,
'message' => 'Category not found.',
], 404);
}
if ($data->image) {
$oldImagePath = public_path($data->image);
if (file_exists($oldImagePath)) {
unlink($oldImagePath);
}
}
$data->delete();
return response()->json([
'success' => true,
'message' => 'Category deleted successfully!',
],200);
}
public function status(int $id): JsonResponse
{
$data = Category::findOrFail($id);
if (!$data) {
return response()->json([
'status' => 'error',
'message' => 'Category not found.',
]);
}
$data->status = $data->status === 'active' ? 'inactive' : 'active';
$data->save();
return response()->json([
'status' => 'success',
'message' => 'Category Status Changed successful!',
]);
}
}
Step 4:
Set up index file :
@extends('backend.app', ['title' => 'Categories'])
@push('styles')
<link href="{{ asset('default/datatable.css') }}" rel="stylesheet" />
@endpush
@section('content')
<!--app-content open-->
<div class="app-content main-content mt-0">
<div class="side-app">
<!-- CONTAINER -->
<div class="main-container container-fluid">
<!-- PAGE-HEADER -->
<div class="page-header">
<div>
<h1 class="page-title">Categories</h1>
</div>
<div class="ms-auto pageheader-btn">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0);">Categories</a></li>
<li class="breadcrumb-item active" aria-current="page">Index</li>
</ol>
</div>
</div>
<!-- PAGE-HEADER END -->
<!-- ROW-4 -->
<div class="row">
<div class="col-12 col-sm-12">
<div class="card product-sales-main">
<div class="card-header border-bottom">
<h3 class="card-title mb-0">Category List</h3>
<div class="card-options ms-auto">
<a href="{{ route('admin.category.create') }}" class="btn btn-primary btn-sm">Add Category</a>
</div>
</div>
<div class="card-body">
<div class="">
<table class="table text-nowrap mb-0 table-bordered" id="datatable">
<thead>
<tr>
<th class="bg-transparent border-bottom-0">ID</th>
<th class="bg-transparent border-bottom-0">Name</th>
{{-- <th class="bg-transparent border-bottom-0">Image</th> --}}
<th class="bg-transparent border-bottom-0">Status</th>
<th class="bg-transparent border-bottom-0">Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div><!-- COL END -->
</div>
<!-- ROW-4 END -->
</div>
</div>
</div>
<!-- CONTAINER CLOSED -->
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
}
});
if (!$.fn.DataTable.isDataTable('#datatable')) {
let dTable = $('#datatable').DataTable({
order: [],
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]
],
processing: true,
responsive: true,
serverSide: true,
language: {
processing: `<div class="text-center">
<img src="{{ asset('default/loader.gif') }}" alt="Loader" style="width: 50px;">
</div>`
},
scroller: {
loadingIndicator: false
},
pagingType: "full_numbers",
dom: "<'row justify-content-between table-topbar'<'col-md-4 col-sm-3'l><'col-md-5 col-sm-5 px-0'f>>tipr",
ajax: {
url: "{{ route('admin.category.index') }}",
type: "GET",
},
columns: [{
data: 'DT_RowIndex',
name: 'DT_RowIndex',
orderable: false,
searchable: false
},
{
data: 'name',
name: 'name',
orderable: true,
searchable: true
},
{
data: 'image',
name: 'image',
orderable: false,
searchable: false
},
{
data: 'status',
name: 'status',
orderable: false,
searchable: false
},
{
data: 'action',
name: 'action',
orderable: false,
searchable: false,
className: 'dt-center text-center'
},
],
});
}
});
// Status Change Confirm Alert
function showStatusChangeAlert(id) {
event.preventDefault();
Swal.fire({
title: 'Are you sure?',
text: 'You want to update the status?',
icon: 'info',
showCancelButton: true,
confirmButtonText: 'Yes',
cancelButtonText: 'No',
}).then((result) => {
if (result.isConfirmed) {
statusChange(id);
}
});
}
// Status Change
function statusChange(id) {
NProgress.start();
let url = "{{ route('admin.category.status', ':id') }}";
$.ajax({
type: "POST",
url: url.replace(':id', id),
success: function(resp) {
NProgress.done();
toastr.success(resp.message);
$('#datatable').DataTable().ajax.reload();
},
error: function(error) {
NProgress.done();
toastr.error(error.message);
}
});
}
// delete Confirm
function showDeleteConfirm(id) {
event.preventDefault();
Swal.fire({
title: 'Are you sure you want to delete this record?',
text: 'If you delete this, it will be gone forever.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!',
}).then((result) => {
if (result.isConfirmed) {
deleteItem(id);
}
});
}
// Delete Button
function deleteItem(id) {
NProgress.start();
let url = "{{ route('admin.category.destroy', ':id') }}";
let csrfToken = '{{ csrf_token() }}';
$.ajax({
type: "DELETE",
url: url.replace(':id', id),
headers: {
'X-CSRF-TOKEN': csrfToken
},
success: function(resp) {
NProgress.done();
toastr.success(resp.message);
$('#datatable').DataTable().ajax.reload();
},
error: function(error) {
NProgress.done();
toastr.error(error.message);
}
});
}
//edit
function goToEdit(id) {
let url = "{{ route('admin.category.edit', ':id') }}";
window.location.href = url.replace(':id', id);
}
</script>
@endpush
Step 5:
Set up create file code:
@extends('backend.app', ['title' => 'Create Category'])
@section('content')
<!--app-content open-->
<div class="app-content main-content mt-0">
<div class="side-app">
<!-- CONTAINER -->
<div class="main-container container-fluid">
<div class="page-header">
<div>
<h1 class="page-title">Categories</h1>
</div>
<div class="ms-auto pageheader-btn">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0);">Categories</a></li>
<li class="breadcrumb-item active" aria-current="page">Create</li>
</ol>
</div>
</div>
<div class="row" id="user-profile">
<div class="col-lg-12">
<div class="tab-content">
<div class="tab-pane active show" id="editProfile">
<div class="card">
<div class="card-body border-0">
<form class="form-horizontal" method="post" action="{{ route('admin.category.store') }}" enctype="multipart/form-data">
@csrf
<div class="row mb-4">
<div class="form-group">
<label for="name" class="form-label">Name:</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" name="name" placeholder="Name" id="" value="{{ old('name') }}">
@error('name')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
{{-- <div class="form-group">
<label for="image" class="form-label">Image:</label>
<input type="file" class="form-control @error('image') is-invalid @enderror" name="image" id="image">
@error('image')
<span class="text-danger">{{ $message }}</span>
@enderror
</div> --}}
<div class="form-group">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CONTAINER CLOSED -->
@endsection
@push('scripts')
@endpush
Step 6:
Set up edit file code:
@section('content')
<!--app-content open-->
<div class="app-content main-content mt-0">
<div class="side-app">
<!-- CONTAINER -->
<div class="main-container container-fluid">
<div class="page-header">
<div>
<h1 class="page-title">Categories</h1>
</div>
<div class="ms-auto pageheader-btn">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0);">Categories</a></li>
<li class="breadcrumb-item active" aria-current="page">Update</li>
</ol>
</div>
</div>
<div class="row" id="user-profile">
<div class="col-lg-12">
<div class="tab-content">
<div class="tab-pane active show" id="editProfile">
<div class="card">
<div class="card-body border-0">
<form class="form-horizontal" method="post" action="{{ route('admin.category.update', $category->id) }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row mb-4">
<div class="form-group">
<label for="name" class="form-label">Name:</label>
<input type="text" class="form-control @error('name') is-invalid @enderror" name="name" placeholder="Name" id="" value="{{ old('name', $category->name) }}">
@error('name')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
{{-- <div class="form-group">
<label for="image" class="form-label">Image:</label>
<input type="file" class="form-control @error('image') is-invalid @enderror" name="image" id="image">
@error('image')
<span class="text-danger">{{ $message }}</span>
@enderror
</div> --}}
<div class="form-group">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CONTAINER CLOSED -->
@endsection
@push('scripts')
@endpush
Step 7:
Set route code:
//! Category Routes
Route::get('category', [CategoryController::class, 'index'])->name('admin.category.index');
Route::get('category/create', [CategoryController::class, 'create'])->name('admin.category.create');
Route::post('category/store', [CategoryController::class, 'store'])->name('admin.category.store');
Route::get('category/edit/{id}', [CategoryController::class, 'edit'])->name('admin.category.edit');
Route::put('category/update/{id}', [CategoryController::class, 'update'])->name('admin.category.update');
Route::delete('category/delete/{id}', [CategoryController::class, 'destroy'])->name('admin.category.destroy');
Route::post('/category/status/{id}', [CategoryController::class, 'status'])->name('admin.category.status');
Note: The file or folder structure will be different based on the project or person.
Subscribe to my newsletter
Read articles from Jalis Mahamud directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Jalis Mahamud
Jalis Mahamud
๐จโ๐ป Laravel Developer | PHP | MySQL | REST APIs Hi, I'm a Laravel developer with experience in building web applications using PHP and modern development practices. I work on backend systems, RESTful APIs, and database structures. I enjoy writing clean code, integrating third-party services, and improving workflows for better performance. ๐ง Tools I often use: Laravel, MySQL, Git, Composer, REST APIs ๐ Focused on: Simplicity, performance, and maintainability