Assume we have this route:
Route::delete('/pages/{page}', [PageController::class, 'destroy'])
->name('site.pages.destroy');
Every delete must:
- Open confirmation modal
- Wait for user confirmation
- 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
- Route is generated once with placeholder
0 - When delete button is clicked → ID is captured
- JavaScript replaces
/0with real ID - 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-deletesends a DELETE requesthx-confirmshows confirmation popup automaticallyhx-targettells HTMX which element to updatehx-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.