Laravel Modal Form Ajax CRUD – Full Guide for Beginners

In this tutorial, we’ll walk through building a Laravel Modal Form Ajax CRUD application step by step. This guide is perfect for beginners who want to learn how to handle Create, Read, Update, and Delete operations in Laravel using Bootstrap modals and Ajax requests. By the end, you’ll have a clean and user-friendly interface powered by Laravel Modal Form Ajax CRUD techniques.

Step 1: Create Laravel Project

Open your Terminal/CMD, go to the project’s root folder, and then run the command below:

composer global require laravel/installer
laravel new laravel-modal-form-ajax-crud

Step 2: Create Database Table

Before we start building our Laravel Modal Form Ajax CRUD system, we need to configure the database connection correctly. This ensures your application can store and retrieve data effectively.

Configure your Laravel project to connect with MySQL or other databases using the .env file — the first step for any Laravel CRUD project.

Open the Command Prompt (CMD), navigate to your project root directory, and run the following command:

php artisan make:migration create_customers_table

Update the up() function with content below

    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name', 50);
            $table->tinyInteger('gender');
            $table->string('email', 100);
            $table->string('image')->nullable();
            $table->timestamps();
        });
    }

Run the migration:

php artisan migrate

Database Error Fix: Specified key was too long

If you encounter the following error during a migration (often when running php artisan migrate):

SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes...

This issue typically occurs when using an older version of MySQL (pre-5.7.7) or an outdated MariaDB version with Laravel’s default settings.

You need to tell the Laravel database schema builder to use a smaller default string length.

Open the file app/Providers/AppServiceProvider.php. add the following line:

use Illuminate\Support\Facades\Schema;

public function boot(): void
{
    // Add this line:
    Schema::defaultStringLength(191);
}

After saving the file, you can try running your migrations again:

php artisan migrate

Step 3: Create Model

Laravel models make it easier to interact with your table and are key to the Laravel Modal Form Ajax CRUD workflow.

Create a file named Customer.php in the PROJECT_ROOT/app/Models directory. You can either use an artisan command or create the file manually.

<?php
// Laravel Modal Form Ajax CRUD - Model
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    //
}

This model allows Laravel to manage customer records and store image file names, making it a core part of our Laravel Modal Form Ajax CRUD setup.

Step 4: Create Controller with Ajax Logic

The controller will handle Ajax-based CRUD operations. This is the heart of your Laravel Modal Form Ajax CRUD system.

Create a file named CustomerController.php in the PROJECT_ROOT/app/Http/Controllers directory. You can either use an artisan command or create the file manually.

<?php
// Laravel Modal Form Ajax CRUD - Controller
namespace App\Http\Controllers;

use App\Models\Customer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class CustomerController extends Controller
{
    public function index(Request $request)
    {
        session()->put('search', $request->get('search', session()->get('search')));
        session()->put('gender', $request->get('gender', session()->get('gender', 0)));
        session()->put('field', $request->get('field', session()->get('field', 'created_at')));
        session()->put('order', $request->get('order', session()->get('order', 'desc')));

        $customers = Customer::when(session()->get('gender') > 0, function ($query) {
            $query->where('gender', session()->get('gender'));
        })
            ->when(session()->get('search'), function ($query) {
                $query->where('name', 'like', '%' . session()->get('search') . '%');
            })->orderBy(session()->get('field'), session()->get('order'))
            ->paginate(5);

        if ($request->ajax())
            return view('customer.index', compact('customers'));
        else
            return view('customer.layout', compact('customers'));
    }

    public function edit($id)
    {
        $customer = Customer::find($id);
        return view('customer.form', compact('customer'));
    }

    public function submit(Request $request)
    {
        // validation
        $rules = [
            'name' => 'required|unique:customers,name,' . $request->id,
            'gender' => 'required',
            'email' => 'required|email:rfc,dns'
        ];
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        // save to database
        if ($request->id > 0) {
            $data = Customer::find($request->id);
        } else {
            $data = new Customer();
        }

        $data->name = $request->name;
        $data->gender = $request->gender;
        $data->email = $request->email;

        // delete uploaded file
        if ($request->is_deleted_image == 1 && $request->id > 0) {
            if (Storage::disk('public')->exists($data->image)) {
                Storage::disk('public')->delete($data->image);
            }
            $data->image = '';
        }
        // upload file
        else if ($request->hasFile('image')) {
            if ($data->image && Storage::disk('public')->exists($data->image)) {
                Storage::disk('public')->delete($data->image);
            }
            $data->image = Storage::disk('public')->put('customer', $request->image);
        }

        $data->save();
        return response()->json([
            'success' => true,
            'redirect_url' => url('/')
        ]);
    }

    public function delete(Request $request)
    {
        $data = Customer::find($request->delete_id);
        // delete uploaded file
        if ($data->image && Storage::disk('public')->exists($data->image)) {
            Storage::disk('public')->delete($data->image);
        }
        $data->delete();
        return redirect('/');
    }
}

This controller ensures a smooth flow for creating, reading, updating, and deleting records while supporting image file uploads in our Laravel Modal Form Ajax CRUD with image upload project.

Step 5: Create Blade Views with Ajax Integration

In this tutorial, we’re using Bootstrap to style the CRUD interface, enhancing the appearance of elements such as buttons, forms, and icons. To get started, you’ll need to install Bootstrap and Bootstrap Icons, which Bootstrap relies on for components like dropdowns and tooltips.

Run the following command to install them via npm:

npm install bootstrap bootstrap-icons

Since Laravel uses Vite as its default frontend asset bundler, we’ll also use it to manage our CSS and JavaScript. If your Laravel project doesn’t have Vite installed yet, you can add it by running:

npm install vite

After installing the necessary packages, you need to import them into your project. Open the resources/js/app.js file and update it as follows:

import './bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.min.css';
import * as bootstrap from 'bootstrap';

window.bootstrap = bootstrap;

We’ll build dynamic Blade views to support the modal form layout and handle Ajax submissions in our Laravel Modal Form Ajax CRUD app.

Create a file named layout.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ env('APP_NAME') }}</title>
    <style>
        form .required label:after {
            content: " *";
            color: red;
            font-weight: bold;
        }

        .loading {
            background: lightgoldenrodyellow url('processing.gif') no-repeat center 65%;
            height: 80px;
            width: 100px;
            position: fixed;
            border-radius: 4px;
            left: 50%;
            top: 50%;
            margin: -40px 0 0 -50px;
            z-index: 2000;
            display: none;
        }
    </style>
    @vite('resources/js/app.js')
</head>

<body style="display: none;">
    <div class="loading"></div>
    <div class="modal fade" id="form_modal" tabindex="-1" aria-hidden="false" data-bs-keyboard="false"
        data-bs-backdrop="static" data-bs-focus="false">
        <div class="modal-dialog modal-lg">
            <div class="modal-content"></div>
        </div>
    </div>
    <div class="fade modal" tabindex="-1" id="alert_modal" aria-hidden="false" data-bs-focus="false">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Alert</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <p></p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Ok</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div class="fade modal" tabindex="-1" id="confirm-delete" aria-hidden="false" data-bs-focus="false">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Confirm Delete</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <p>Are you sure want to delete?</p>
                </div>
                <div class="modal-footer">
                    <form id="frm_delete" action="{{ url('/delete') }}" method="post"
                        style="padding-bottom: 0px;margin-bottom: 0px;">
                        @method('DELETE')
                        @csrf
                        <input type="hidden" name="delete_id" id="delete_id" />
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                        <button type="submit" class="btn btn-danger" data-bs-dismiss="modal">Delete</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-success">
        <div class="container-fluid">
            <a class="navbar-brand fw-bold" href="{{ url('/') }}">Laravel Center</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
                aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
        </div>
    </nav>
    <main style="margin-top: 100px;" id="content">
        @include('customer.index')
    </main>
</body>

<script>
    document.addEventListener("DOMContentLoaded", () => {
        document.body.style.display = "block";
        const formModal = document.getElementById("form_modal");
        const popupModal = new bootstrap.Modal(formModal);
        if (formModal) {
            formModal.addEventListener("shown.bs.modal", (event) => {
                const autoFocusElement = formModal.querySelector('input#name');
                autoFocusElement.focus();
                autoFocusElement.select();
            });
            formModal.addEventListener("hide.bs.modal", (event) => {
                document.activeElement?.blur();
            });

        }

        const deleteModal = document.getElementById("confirm-delete");
        if (deleteModal) {
            deleteModal.addEventListener("show.bs.modal", (event) => {
                const triggerElement = event.relatedTarget;
                const recordId = triggerElement?.dataset.recordId;

                if (recordId) {
                    const deleteInput = document.querySelector("input#delete_id");
                    if (deleteInput) {
                        deleteInput.value = recordId;
                    }
                }
            });
            deleteModal.addEventListener("hide.bs.modal", (event) => {
                document.activeElement?.blur();
            });
        }

        document.addEventListener("click", function(event) {
            if (event.target.matches("a.page-link")) {
                event.preventDefault();
                ajaxLoad(event.target.getAttribute("href"));
            }
        });

        document.addEventListener("submit", function(event) {
            if (event.target.matches("form#frm")) {
                event.preventDefault();
                document.querySelector(".loading").style.display = "block";

                const form = event.target;
                const formData = new FormData(form);
                const url = form.getAttribute("action");
                const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute(
                    "content");

                fetch(url, {
                        method: "POST",
                        headers: {
                            "X-CSRF-TOKEN": csrfToken
                        },
                        body: formData
                    })
                    .then(response => response.json())
                    .then(data => {
                        document.querySelectorAll(".is-invalid").forEach(el => el.classList.remove(
                            "is-invalid"));
                        document.querySelectorAll("span.invalid-feedback").forEach(el => el
                            .remove());

                        if (!data.success) {
                            for (let control in data.errors) {
                                const input = document.getElementById(control);
                                if (input) {
                                    input.classList.add("is-invalid");

                                    const errorSpan = document.createElement("span");
                                    errorSpan.className = "invalid-feedback";
                                    errorSpan.textContent = data.errors[control];

                                    input.parentNode.insertBefore(errorSpan, input.nextSibling);
                                }
                            }
                        } else {
                            ajaxLoad(data.redirect_url);
                            popupModal.hide();
                        }

                        document.querySelector(".loading").style.display = "none";
                    })
                    .catch(error => {
                        alert("Error: " + error);
                    });
            }

            if (event.target.matches("form#frm_delete")) {
                event.preventDefault();
                document.querySelector(".loading").style.display = "block";

                const form = event.target;
                const formData = new FormData(form);
                const url = form.getAttribute("action");
                const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute(
                    "content");

                fetch(url, {
                        method: "POST",
                        headers: {
                            "X-CSRF-TOKEN": csrfToken
                        },
                        body: formData
                    })
                    .then(response => response.text())
                    .then(data => {
                        document.getElementById("content").innerHTML = data;
                        document.querySelector(".loading").style.display = "none";
                    })
                    .catch(error => {
                        alert("Error: " + error);
                    });
            }

            if (event.target.matches("form#search_form")) {
                event.preventDefault();
                const form = event.target;
                const formData = new FormData(form);
                const queryString = new URLSearchParams(formData).toString();
                const url = form.getAttribute("action") + "?" + queryString;
                ajaxLoad(url);
            }
        });

        window.ajaxLoad = (url, disaplyArea = "content") => {
            document.querySelector(".loading").style.display = "block";

            fetch(url, {
                    method: "GET",
                })
                .then(response => response.text())
                .then(data => {
                    document.getElementById(disaplyArea).innerHTML = data;
                    document.querySelector(".loading").style.display = "none";
                })
                .catch(error => {
                    alert("Error: " + error);
                });
        };

        window.changeProfile = () => {
            document.getElementById('image').click();
        };

        window.validateFile = (inputElement, maxFileSize = 1, validFileExtension = [".jpg", ".jpeg", ".bmp",
            ".gif", ".png",
            ".webp"
        ]) => {
            let image = null;
            const files = inputElement.files;

            if (files && files.length > 0) {
                const alertModal = document.getElementById("alert_modal");
                const modal = new bootstrap.Modal(alertModal);
                const fileSizeMB = ((files[0].size / 1024) / 1024).toFixed(4);
                const fileName = files[0].name;

                if (fileSizeMB <= maxFileSize) {
                    let isValidExtension = false;
                    let fileExtension = '';
                    for (let ext of
                            validFileExtension) {
                        if (fileName.toLowerCase().endsWith(ext.toLowerCase())) {
                            isValidExtension = true;
                            fileExtension = ext;
                            break;
                        }
                    }
                    const baseName = fileName.substring(0, fileName.length -
                        fileExtension.length).trim();
                    const cleanedBaseName = baseName.replace(/[^a-zA-Z0-9-_\s]/g, '');
                    if (cleanedBaseName !== baseName) {
                        alertModal.querySelector('.modal-body p').textContent =
                            "Invalid filename. Filename only allows alphanumeric characters.";
                        modal.show();
                        inputElement.value = '';
                    } else if (isValidExtension) {
                        image = files[0];
                    } else {
                        alertModal.querySelector('.modal-body p').textContent = fileName +
                            " is invalid. Allowed extensions are: " + validFileExtension.join(", ");
                        modal.show();
                        inputElement.value = '';
                    }

                } else {
                    const maxSizeMsg = (maxFileSize < 1) ?
                        " Maximum file size is " + (maxFileSize * 1000).toString() + " KB." :
                        "Maximum file size is " +
                        maxFileSize.toString() + "MB.";
                    alertModal.querySelector('.modal-body p').textContent = maxSizeMsg;
                    modal.show();
                    inputElement.value = '';
                }
            }
            return image;
        };

        window.removeProfile = () => {
            document.getElementById('img_preview').setAttribute("src", "{{ url('./default.png') }}");
            document.getElementById('is_deleted_image').value = 1;
        };

        document.addEventListener('change', function(event) {
            if (event.target.matches("input[type='file'][name='image']")) {
                if (event.target.value !== '') {
                    const
                        selectedFile = validateFile(event.target, 1, [".jpg", ".jpeg", ".bmp", ".gif",
                            ".png",
                            ".webp"
                        ]);
                    if (selectedFile) {
                        const reader = new FileReader();
                        reader.onload = function(e) {
                            document.getElementById('img_preview').setAttribute('src', e.target
                                .result);
                        }
                        reader.readAsDataURL(selectedFile);
                    }
                }
            }
        });

        window.ajaxPopup = function(url, show_loading = false) {
            if (show_loading) {
                document.querySelectorAll('.loading').forEach(el => el.style.display = 'block');
            }

            fetch(url, {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json'
                    }
                })
                .then(response => {
                    if (!response.ok) throw new Error('Failed to load modal content');
                    return response.text();
                })
                .then(data => {
                    const modalContent = formModal.querySelector('.modal-content');
                    if (modalContent) {
                        modalContent.innerHTML = data;
                    }

                    if (show_loading) {
                        document.querySelectorAll('.loading').forEach(el => el.style.display = 'none');
                    }
                    popupModal.show();
                })
                .catch(error => {
                    alert(error.message);
                });
        };
    });
</script>

</html>

Create a file named index.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.

<div class="container">
    <div class="pagetitle">
        <a type="button" class="btn btn-primary" style="float: right" href="javascript:ajaxPopup('{{ url('/create') }}')">
            <i class="bi bi-plus-circle"></i> Add New
        </a>
        <h2>Customers List</h2>
        <hr />
    </div>
    <form method="get" id="search_form" action="{{ url('/') }}">
        <div class="row pb-3">
            <div class="col-md-10">
                <div class="row justify-content-start">
                    <div class="col-lg-3 col-sm-6">
                        <label class="form-label mb-0">Name</label>
                        <input type="text" class="form-control" id="search" name="search"
                            value="{{ session('search') }}" placeholder="Search..." />
                    </div>
                    <div class="col-lg-3 col-sm-6">
                        <label class="form-label mb-0">Gender</label>
                        <select class="form-select" id="gender" name="gender">
                            <option value="0" {{ session('gender') == 0 ? 'selected' : '' }}>ALL</option>
                            <option value="2" {{ session('gender') == 2 ? 'selected' : '' }}>Male</option>
                            <option value="1" {{ session('gender') == 1 ? 'selected' : '' }}>Female</option>
                        </select>
                    </div>
                </div>
            </div>
            <div class="col-md-2 align-self-end">
                <button type="submit" class="btn btn-secondary pt-1" style="float: right">
                    <i class="bi bi-search"></i> Search
                </button>
            </div>
        </div>
    </form>
    <table class="table table-striped">
        <thead class="table-dark">
            <tr>
                <th width="60px" class="text-center">No</th>
                <th width="150px" class="text-center">Photo</th>
                <th style="cursor: pointer"
                    onclick="ajaxLoad('{{ url('/?field=name&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
                    Name
                    <i
                        class="text-secondary {{ session('field') == 'name'
                            ? (session('order') == 'desc'
                                ? 'bi bi-sort-alpha-down-alt'
                                : 'bi bi-sort-alpha-down')
                            : 'bi bi-arrow-down-up' }}"></i>
                </th>
                <th style="cursor: pointer"
                    onclick="ajaxLoad('{{ url('/?field=gender&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
                    Gender
                    <i
                        class="text-secondary {{ session('field') == 'gender'
                            ? (session('order') == 'desc'
                                ? 'bi bi-sort-alpha-down-alt'
                                : 'bi bi-sort-alpha-down')
                            : 'bi bi-arrow-down-up' }}"></i>
                </th>
                <th style="cursor: pointer"
                    onclick="ajaxLoad('{{ url('/?field=email&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
                    Email
                    <i
                        class="text-secondary {{ session('field') == 'email'
                            ? (session('order') == 'desc'
                                ? 'bi bi-sort-alpha-down-alt'
                                : 'bi bi-sort-alpha-down')
                            : 'bi bi-arrow-down-up' }}"></i>
                </th>
                <th style="cursor: pointer"
                    onclick="ajaxLoad('{{ url('/?field=created_at&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
                    Created at
                    <i
                        class="text-secondary {{ session('field') == 'created_at'
                            ? (session('order') == 'desc'
                                ? 'bi bi-sort-alpha-down-alt'
                                : 'bi bi-sort-alpha-down')
                            : 'bi bi-arrow-down-up' }}"></i>
                </th>
                <th width="150px" class="text-center">Action</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($customers as $index => $value)
                <tr>
                    <th style="vertical-align: middle;text-align: center">
                        {{ $customers->perPage() * ($customers->currentPage() - 1) + ($index + 1) }}
                    </th>
                    <td class="text-center py-2"><img style="width: 60px;height: 60px;"
                            src="{{ url($value->image ? './storage/' . $value->image : './default.png') }}" />
                    </td>
                    <td style="vertical-align: middle">{{ $value->name }}</td>
                    <td style="vertical-align: middle">{{ $value->gender == 2 ? 'Male' : 'Female' }}</td>
                    <td style="vertical-align: middle">{{ $value->email }}</td>
                    <td style="vertical-align: middle">{{ date('d/m/Y H:i:s', strtotime($value->created_at)) }}</td>
                    <td style="vertical-align: middle;text-align: center;">
                        <i class="bi bi-trash3-fill text-danger" role="button" data-record-id="{{ $value->id }}"
                            title="Delete" data-bs-toggle="modal" data-bs-target="#confirm-delete"></i>
                        <a title="Edit" href="javascript:ajaxPopup('{{ url('/edit/' . $value->id) }}',true)">
                            <i class="bi bi-pencil-square text-success ps-3" role="button"></i>
                        </a>
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
    <nav>
        <ul class="pagination justify-content-end">
            {{ $customers->links() }}
        </ul>
    </nav>
</div>

Create a file named form.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.

<div class="modal-header py-2 bg-secondary text-light">
    <h5 class="modal-title" style="font-weight: bold">
        {{ isset($customer) ? 'Edit' : 'New' }} Customer
    </h5>
</div>
<div class="modal-body">
    <form method="POST" id="frm" enctype="multipart/form-data" action="{{ url('/submit') }}">
        @csrf
        @method(isset($customer) ? 'PUT' : 'POST')
        <input type="hidden" value="{{ isset($customer) ? $customer->id : 0 }}" name="id" />
        <div class="row">
            <div class="col-md-6">
                <div class="required mb-3">
                    <label for="name" class="form-label">Name</label>
                    <input id="name" name="name" type="text" class="form-control"
                        value="{{ isset($customer) ? $customer->name : '' }}" />
                </div>
                <div class="required mb-3">
                    <label for="gender" class="form-label">Gender</label>
                    <select id="gender" name="gender" class="form-select">
                        <option value="2" @if (isset($customer) ? $customer->gender : '' == 'Male') selected @endif>Male
                        </option>
                        <option value="1" @if (isset($customer) ? $customer->gender : '' == 'Female') selected @endif>Female
                        </option>
                    </select>
                </div>
                <div class="required  mb-3">
                    <label for="email" class="form-label">Email</label>
                    <input id="email" name="email" type="text"
                        value="{{ isset($customer) ? $customer->email : '' }}" class="form-control">
                </div>
            </div>
            <div class="col-md-6">
                <div class="mb-3 text-center">
                    <label class="form-label">Photo</label>
                    <div>
                        <input type="hidden" style="display: none" value="0" name="is_deleted_image"
                            id="is_deleted_image">
                        <img id="img_preview" style="width: 180px;height: 180px;cursor: pointer;"
                            class="img img-thumbnail" onerror="this.onerror=null;this.src='./default.png'"
                            src="{{ isset($customer) && $customer->image ? url('./storage/' . $customer->image) : url('./default.png') }}"
                            onclick="changeProfile()" />
                        <p>
                            <a href="javascript:changeProfile()" style="text-decoration: none;">
                                Change</a>
                            <a href="javascript:removeProfile()" style="color: red;text-decoration: none;">Remove</a>
                        </p>
                        <input type="file" id="image" name="image" style="display: none;"
                            accept=".jpg,.jpeg,.bmp,.gif,.png,.webp" />
                    </div>
                </div>
            </div>
        </div>

    </form>
</div>
<div class="modal-footer">
    <button type="button" class="btn btn-danger" data-bs-dismiss="modal">
        <i class="bi bi-x-lg"></i> Cancel
    </button>
    <button type="submit" class="btn btn-primary" form="frm">
        <i class="bi bi-floppy" style="padding-right: 3px;"></i>Save</button>
</div>

Step 6: Define Routes (Standard and Ajax)

Routes connect everything together in your Laravel Modal Form Ajax CRUD project—ensuring all Ajax and modal endpoints work properly.

In routes/web.php:

<?php
// Laravel Modal Form Ajax CRUD - Controller

use App\Http\Controllers\CustomerController;
use Illuminate\Support\Facades\Route;

Route::controller(CustomerController::class)->group(function () {
    Route::get('/', 'index');
    Route::view('create',  'customer.form');
    Route::get('edit/{id}',  'edit');
    Route::match(['post', 'put'], 'submit', 'submit');
    Route::delete('delete', 'delete');
});

Additional Setup

💡 Tip: To ensure the project loads smoothly without broken images, you can download this default image and place it inside your Laravel project’s public/ folder. This helps prevent errors when no uploaded images are available yet.

Fixing Pagination Styling with Bootstrap in Laravel

By default, Laravel uses Tailwind CSS for pagination views. If your project is using Bootstrap (especially Bootstrap 4 or 5), the pagination links generated by {{ $items->links() }} may look unstyled or broken.

To fix this, you need to tell Laravel to use Bootstrap-compatible pagination views. You can do this by updating the AppServiceProvider.

Open the file:
app/Providers/AppServiceProvider.php

Inside the boot() method, add the following line:

use Illuminate\Pagination\Paginator;

public function boot()
{
    Paginator::useBootstrap();
}

This tells Laravel to render pagination links using Bootstrap’s markup instead of Tailwind CSS.

When you’re handling image uploads in Laravel, the uploaded files are typically stored in the storage/app/public directory. However, these files need to be publicly accessible from the browser. Laravel provides a convenient command to create a symbolic link between the public/storage directory and storage/app/public:

php artisan storage:link

This command will generate a symbolic link so you can access uploaded images using a public URL like /storage/your-image.jpg.

Running and Testing Your Project

Open your Terminal/CMD in separate windows, go to the project’s root folder, and then run the command below:

npm run dev
php artisan serve

With both commands running in their separate windows, open your web browser to the Laravel address (http://127.0.0.1:8000). You should now see the customer list with image upload, search, sort, and pagination features working as expected.

By following this guide, you now have a solid understanding of building a full-featured Laravel Modal Form Ajax CRUD with image upload, search, sort, and pagination.

Conclusion

You’ve now learned how to build a complete Laravel Modal Form Ajax CRUD system step by step. By combining Laravel’s robust backend with Ajax and Bootstrap modals, you can perform create, read, update, and delete operations without reloading the page — giving users a smooth and modern experience.

This Laravel Modal Form Ajax CRUD tutorial helps you understand how to handle data dynamically, validate forms, and update your interface instantly. You can further enhance it by adding image uploads, search, or pagination features to make it even more powerful.

Mastering Laravel Modal Form Ajax CRUD is an essential skill for building interactive and efficient web applications.

Senghok
Senghok

Senghok is a web developer who enjoys working with Laravel and Vue.js. He creates easy-to-follow tutorials and guides to help beginners learn step by step. His goal is to make learning web development simple and fun for everyone.

Articles: 44

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *