In this blog post, we ‘ll understand how HTMX works, and build a very basic CRUD in Laravel using HTMX.
Table of Contents
- What is HTMX?
- Magical HTMX Attributes
- Setting Up the Laravel Playground
- Let’s HTMXify the Laravel App
- Final Note
What is HTMX?
HTMX (HTML extensions) is an easy-to-use JavaScript library that allows you to build reactive user interfaces directly in HTML.
It extends familiar HTML attributes with superpowers that allow you to build reactive UIs directly in your markup.
Here is how the overall workflow :
- The user triggers an event (like a click or keyup) in the browser.
- The browser passes this event to HTMX.
- HTMX issues an AJAX request to the server.
- The server responds with HTML.
- HTMX updates the DOM with the new HTML.
- The browser displays the updated page to the user.
Magical HTMX Attributes
HTMX enriches standard HTML by introducing a set of new attributes. These attributes empower HTML elements to initiate HTTP requests, trigger events, specify targets, and control the content-swapping process.
Some of the key HTMX attributes include:
-
hx-{get, post, put, delete}: These attributes define the HTTP verb for the request, allowing elements to issue
GET
,POST
,PUT
andDELETE
requests. -
hx-trigger: This attribute defines the event that initiates the request like based on events such as
mouseover
or other custom interactions. Typically, HTMX triggers request automatically for events like buttonclicks
or formsubmissions
. -
hx-target: This attribute enables us to specify the target element where the response content will be placed.
-
hx-swap: The
hx-swap
attribute determines how the response content will replace the target element. There are different swap strategies likeinnerHTML
,outerHTML
,delete
and few more.
Setting Up the Laravel Playground
Before we can start enhancing things with HTMX, we need a Laravel app to play with.
Laravel is a great PHP framework for building web apps, so let’s use it to spin up a basic CRUD example.
First, we’ll scaffold out a Contact model and controller using Laravel’s artisan command:
php artisan make:model Contact --controller --migration
This gives us the model, migration, and controller we need to get started.
Next, we’ll add a resource route for the ContactController in routes/web.php
Route::resource('contacts', ContactController::class);
I won’t bore you with nitty-gritty of the CRUD application details since it’s quite standard. Let’s skip that part, but you can have a look at that here!
Now that our Laravel playground is set up, it’s time for the fun part - integrating HTMX!
Let’s HTMXify the Laravel App
First, we need to install HTMX and make it available to our Laravel project. There are a couple options to install HTMX, but we will use CDN approach to get started quickly.
Let’s include the CDN link in layout app.blade.php
<script src="https://unpkg.com/[email protected]"
integrity="sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO"
crossorigin="anonymous"></script>
Now that we’ve got HTMX added to our toolkit, let’s start using it!
Search Contacts
Our contact list page has a standard server-rendered table. Functional but a bit dull. Let’s spice it up with some client-side magic using HTMX.
The plan:
- Wire up the search input to fetch contacts
- Target just the table body to refresh
- Check for HTMX request in controller
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Contacts
</h2>
</x-slot>
<div id="content">
<div class="flex justify-between p-3">
<!-- Removed -->
<form action="{{ route('contacts.index') }}" class="flex gap-2">
<x-text-input name="q"
class="px-2 py-1 block" :value="request('q')"/>
<x-secondary-button type="submit" class="px-4 py-2">
Search
</x-secondary-button>
</form>
<!-- Added -->
<x-text-input name="q" class="px-2 py-1 block" :value="request('q')"
placeholder="Search contacts..."
hx-get="{{ route('contacts.index') }}"
hx-target="#contacts-table-body"
hx-trigger="keyup changed delay:500ms, search"/>
<x-primary-link href="{{ route('contacts.create') }}">
Create New Contact
</x-primary-link>
</div>
<table id="contacts-table" class="table-auto w-full">
<thead>
<!-- Table header here. -->
</thead>
<tbody id="contacts-table-body">
@include('contacts.partials.table-body')
</tbody>
</table>
</div>
</x-app-layout>
class ContactController extends Controller
{
public function index(Request $request)
{
$searchTerm = $request->input('q');
$contacts = Contact::where('name', 'LIKE', "%$searchTerm%")->get();
// Added
if ($request->header('hx-request')) {
return view('contacts.partials.table-body', compact('contacts'));
}
return view('contacts.index', compact('contacts'));
}
...
...
}
So just with very few lines of code, we have made reactive search on table
Create Contact
Let’s make creating new contacts slick and reactive with HTMX. First, we’ll load the create form asynchronously when clicking “New Contact”:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Contacts
</h2>
</x-slot>
<!-- Added -->
<div id="section">
{{-- Placeholder for the views --}}
</div>
<div id="content">
<div class="flex justify-between p-3">
<x-text-input name="q" class="px-2 py-1 block"
:value="request('q')" placeholder="Search contacts..."
hx-get="{{ route('contacts.index') }}"
hx-target="#contacts-table-body"
hx-trigger="keyup changed delay:500ms, search"/>
<!-- Removed -->
<x-primary-link href="{{ route('contacts.create') }}">
Create New Contact
</x-primary-link>
<!-- Added -->
<x-primary-link hx-get="{{ route('contacts.create') }}"
hx-target="#section">
Create New Contact
</x-primary-link>
</div>
<table id="contacts-table" class="table-auto w-full">
...
</table>
</div>
</x-app-layout>
Then we’ll submit the form without reloading using hx-post:
<div id="partialCreate" class="p-5 border-b-8 border-b-gray-100">
<form hx-post="{{ route('contacts.store') }}"
hx-target="#partialCreate"
hx-swap="delete">
@csrf
@include('contacts.partials.form')
</form>
</div>
On success, we want to refresh the table without reloading the page. We can achieve this by sending an HX-Trigger
header from the server, this will act as an event send from server side to tell the client to perform certain action.
class ContactController extends Controller
{
...
public function create()
{
return view('contacts.create');
}
public function store(ContactRequest $request)
{
$contact = Contact::create($request->all());
return response()->make($contact, 200, ['HX-Trigger' => 'loadContacts']);
}
...
...
}
Back in index.blade.php
, we need to update the table to listen for the loadContacts
event sent by the server. This tells HTMX to fire off a request to get fresh table data from the route we specified with hx-get. We add the hx-get
and hx-trigger
attribute:
<x-app-layout>
...
<div id="content">
...
<table id="contacts-table" class="table-auto w-full">
<thead>
<!-- Table header here. -->
</thead>
<tbody id="contacts-table-body"
hx-get="{{ route('contacts.index') }}"
hx-trigger="loadContacts from:body">
@include('contacts.partials.table-body')
</tbody>
</table>
</div>
</x-app-layout>
So the flow is:
- Server sends “loadContacts” event on contact create
- hx-trigger sees the event
- Makes a request to load fresh table data
- Swaps in the new data without reloading
Here it’s in action.
View/Edit/Delete Contact
Now that we’ve seen HTMX in action for search and create, let’s quickly implement the rest of CRUD.
In table-row.blade.php
, we can add HTMX attributes for smooth view, edit and delete:
<tr id="contact-{{ $contact->id }}">
<td class="px-4 py-2 border">{{ $contact->name }}</td>
<td class="px-4 py-2 border">{{ $contact->email }}</td>
<td class="px-4 py-2 border">{{ $contact->phone }}</td>
<td class="px-4 py-2 border">{{ $contact->address }}</td>
<td class="px-4 py-2 border">
<a class="mr-1 uppercase hover:underline"
hx-get="{{ route('contacts.show', $contact->id) }}"
hx-target="#section">View</a>
<a class="mr-1 uppercase hover:underline"
hx-get="{{ route('contacts.edit', $contact->id) }}"
hx-target="#section">Edit</a>
<a class="mr-1 uppercase hover:underline"
hx-delete="{{ route('contacts.destroy', $contact->id) }}"
hx-confirm="Are you sure you want to delete this contact?"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>Delete</a>
</td>
</tr>
In edit.blade.php, we use HTMX to submit the edit form asynchronously:
hx-put
attribute to send a PUT request on form submithx-target="#partialEdit"
set the target to be swappedhx-swap="delete"
specifies the swap strategy. For example, here it willdelete
the target#partialEdit
after the server response has been received.
<div id="partialEdit" class="p-5 border-b-8 border-b-gray-100">
<form hx-put="{{ route('contacts.update', $contact->id) }}"
hx-target="#partialEdit"
hx-swap="delete">
@csrf
@method('PUT')
@include('contacts.partials.form')
</form>
</div>
In show.blade.php, we add HTMX to navigate without full page reloads:
hx-get
to fetch pages asynchronouslyhx-target
to update the content areahx-swap
specifies the swap strategy. Here it means we want to delete the target#partialShow
on server response.
<div id="partialShow" class="p-5 border-b-8 border-b-gray-100">
<h2 class="text-2xl font-bold">{{ $contact->name }}</h2>
<p class="text-gray-600">Email: {{ $contact->email }}</p>
<p class="text-gray-600">Phone: {{ $contact->phone }}</p>
<p class="text-gray-600">Address: {{ $contact->address }}</p>
<div class="flex items-center gap-2 mt-4">
<x-primary-button hx-get="{{ route('contacts.edit', $contact->id) }}"
hx-target="#section">Edit</x-primary-button>
<x-secondary-button hx-get="{{ route('contacts.index') }}"
hx-target="#partialShow"
hx-swap="delete">Go Back</x-secondary-button>
</div>
</div>
As we wrap up, I would like to share our complete ContactController
.
class ContactController extends Controller
{
public function index(Request $request)
{
$searchTerm = $request->input('q');
$contacts = Contact::where('name', 'LIKE', "%$searchTerm%")->get();
if ($request->header('hx-request')) {
return view('contacts.partials.table-body', compact('contacts'));
}
return view('contacts.index', compact('contacts'));
}
public function create()
{
return view('contacts.create');
}
public function store(ContactRequest $request)
{
$contact = Contact::create($request->all());
return response()->make($contact, 200, ['HX-Trigger' => 'loadContacts']);
}
public function show(Contact $contact)
{
return view('contacts.show', compact('contact'));
}
public function edit(Contact $contact)
{
return view('contacts.edit', compact('contact'));
}
public function update(ContactRequest $request, Contact $contact)
{
$contact->update($request->all());
return response()->make($contact, 200, ['HX-Trigger' => 'loadContacts']);
}
public function destroy(Contact $contact)
{
$contact->delete();
return response()->make($contact, 200, ['HX-Trigger' => 'loadContacts']);
}
}
Final Note
And that wraps up our introduction to using HTMX in Laravel! As we have seen it just took a few lines of code to make this reactive. How cool is that? You can find the complete source code for this implementation on GitHub: Project Link.
While we covered the basics here, there is a LOT more we can do with HTMX like advanced swapping, animations, plugins, and more. Check out the HTMX documentation for further details and examples.
Make sure to follow me on Twitter to get notified when I publish more content.