Assume we have this route:

                                Route::delete('/pages/{page}', [PageController::class, 'destroy'])
    ->name('site.pages.destroy');
                            

Every delete must:

  1. Open confirmation modal
  2. Wait for user confirmation
  3. Then trigger DELETE request

We are only discussing how the front end triggers the route.


1. One Form Per Row

Each row contains its own form.

Blade

                                @foreach ($pages as $page)
<tr>
    <td>{{ $page->title }}</td>
    <td>
        <form id="delete-form-{{ $page->id }}"
              action="{{ route('site.pages.destroy', $page->id) }}"
              method="POST">
            @csrf
            @method('DELETE')

            <button type="button"
                    onclick="openModal({{ $page->id }})">
                Delete
            </button>
        </form>
    </td>
</tr>
@endforeach
                            

JavaScript

                                <script>
let selectedId = null;

function openModal(id) {
    selectedId = id;
    document.getElementById('delete-user-modal').classList.remove('hidden');
}

function submitDelete() {
    document
        .getElementById('delete-form-' + selectedId)
        .submit();
}
</script>
                            

What’s Happening

  • Each row has its own form
  • Modal opens
  • On confirmation → that specific form submits

Works fine.

If you render 300 rows, you now have 300 forms sitting in your DOM like unpaid interns.


2. Ajax / Fetch DELETE Request

Here, rows do not contain forms.

Blade

                                @foreach ($pages as $page)
<tr id="row-{{ $page->id }}">
    <td>{{ $page->title }}</td>
    <td>
        <button type="button"
                onclick="openModal({{ $page->id }})">
            Delete
        </button>
    </td>
</tr>
@endforeach
                            

Layout (CSRF Meta)

                                <meta name="csrf-token" content="{{ csrf_token() }}">
                            

JavaScript

                                <script>
let selectedId = null;

function openModal(id) {
    selectedId = id;
    document.getElementById('delete-user-modal').classList.remove('hidden');
}

function submitDelete() {

    fetch(`/pages/${selectedId}`, {
        method: 'DELETE',
        headers: {
            'X-CSRF-TOKEN': document
                .querySelector('meta[name="csrf-token"]')
                .getAttribute('content'),
            'Content-Type': 'application/json'
        }
    }).then(response => {
        if (response.ok) {
            document.getElementById('row-' + selectedId).remove();
        }
    });
}
</script>
                            

What’s Happening

  • Modal confirms
  • JavaScript sends DELETE request
  • On success → remove row from DOM

No page reload. Clean UI.

But you are now responsible for handling errors properly.


3. One Form Wrapping the Table

Instead of many forms, use one.

Blade

                                <form id="delete-form" method="POST">
    @csrf
    @method('DELETE')

    <input type="hidden" name="page" id="delete-id">

    <table>
        @foreach ($pages as $page)
        <tr>
            <td>{{ $page->title }}</td>
            <td>
                <button type="button"
                        onclick="openModal({{ $page->id }})">
                    Delete
                </button>
            </td>
        </tr>
        @endforeach
    </table>
</form>
                            

JavaScript

                                <script>
let selectedId = null;

function openModal(id) {
    selectedId = id;
    document.getElementById('delete-user-modal').classList.remove('hidden');
}

function submitDelete() {

    const form = document.getElementById('delete-form');

    form.action = `/pages/${selectedId}`;
    form.submit();
}
</script>
                            

What’s Happening

  • One form
  • Hidden input stores ID
  • Action set dynamically
  • Full page reload

Cleaner than method one. Still traditional submission.


4. Hidden Global Form + Route Placeholder (Preferred)

This is your pattern.

One hidden form. Outside the table. No ID in the initial action.

Hidden Form

                                <form method="POST"
      action="{{ route('site.pages.destroy', ['page' => 0]) }}"
      id="delete-form"
      class="hidden">
    @csrf
    @method('DELETE')
</form>
                            

Notice:

We deliberately pass 0 as a placeholder.

Laravel generates a valid route:

                                /pages/0
                            

We will replace 0 dynamically.


Delete Button

                                <button type="button"
        data-modal-target="delete-user-modal"
        data-modal-toggle="delete-user-modal"
        onclick="setID({{ $page->id }})">
    Delete
</button>
                            

JavaScript

                                <script>
let selectedId = null;

function setID(id) {

    selectedId = id;

    let path = "{{ route('site.pages.destroy', ['page' => 0]) }}";

    path = path.replace('/0', '/' + id);

    document.getElementById('delete-form').action = path;
}

function submitForm() {
    document.getElementById('delete-form').submit();
}
</script>
                            

Modal Confirmation Button

                                <a href="#" onclick="submitForm()">
    Yes
</a>
                            

What’s Happening

  1. Route is generated once with placeholder 0
  2. When delete button is clicked → ID is captured
  3. JavaScript replaces /0 with real ID
  4. Confirmation button submits the hidden form

You get:

  • One form in DOM
  • Clean table markup
  • Proper Laravel route usage
  • No route string hardcoded
  • Centralized delete logic
  • Modal confirmation built-in

Structurally, this is clean and scalable.


Bonus: The HTMX Way

If you’re using HTMX, you don’t need custom fetch calls, and you don’t need to manually submit hidden forms.

HTMX lets you trigger HTTP requests directly from HTML attributes.

Yes. Just attributes.


Basic HTMX Delete Example

First, include HTMX in your layout:

                                <script src="https://unpkg.com/htmx.org@1.9.10"></script>
<meta name="csrf-token" content="{{ csrf_token() }}">
                            

Now your table:

                                @foreach ($pages as $page)
<tr id="row-{{ $page->id }}">
    <td>{{ $page->title }}</td>
    <td>
        <button 
            hx-delete="{{ route('site.pages.destroy', $page->id) }}"
            hx-target="#row-{{ $page->id }}"
            hx-swap="outerHTML"
            hx-confirm="Are you sure you want to delete this page?"
            hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
        >
            Delete
        </button>
    </td>
</tr>
@endforeach
                            

What’s Happening

  • hx-delete sends a DELETE request
  • hx-confirm shows confirmation popup automatically
  • hx-target tells HTMX which element to update
  • hx-swap="outerHTML" removes the row after successful response
  • CSRF token is passed via header

No custom JavaScript required.

No hidden forms.

No manual fetch logic.

Just attributes.

Which Method Is Better?

With confirmation required, method one becomes noisy because you must track many forms.

Method three is acceptable but still tied to page reload behavior.

Ajax is powerful if you want instant UI updates.

But from a structural Laravel perspective, the hidden global form with placeholder replacement is the most balanced:

  • Clean Blade
  • Clean routes
  • Clean confirmation flow
  • Minimal DOM pollution
  • Easy to maintain

It separates concerns properly.

Table renders data. Modal handles confirmation. Hidden form handles submission.

Why HTMX Is Interesting

It gives you:

  • AJAX behavior
  • Confirmation
  • DOM update
  • RESTful requests

Without writing JavaScript logic yourself.

It keeps your Blade readable.

It keeps your routes clean.

It feels like traditional server-driven apps, but dynamic.

Hope this gives you clearer picture what you can adopt to delete a row.

Comments


Comment created and will be displayed once approved.