Support the ongoing development of Laravel.io →
Article Hero Image

Getting Started with HTMX in Laravel - An Overview

20 Sep, 2023 8 min read

Photo by Florian Olivo on Unsplash

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?

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 :

  1. The user triggers an event (like a click or keyup) in the browser.
  2. The browser passes this event to HTMX.
  3. HTMX issues an AJAX request to the server.
  4. The server responds with HTML.
  5. HTMX updates the DOM with the new HTML.
  6. 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:

  1. hx-{get, post, put, delete}: These attributes define the HTTP verb for the request, allowing elements to issue GET, POST, PUT and DELETE requests.

  2. 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 button clicks or form submissions.

  3. hx-target: This attribute enables us to specify the target element where the response content will be placed.

  4. hx-swap: The hx-swap attribute determines how the response content will replace the target element. There are different swap strategies like innerHTML, 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.

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>

And let's check the request type in controller,

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

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 submit
  • hx-target="#partialEdit" set the target to be swapped
  • hx-swap="delete" specifies the swap strategy. For example, here it will delete 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 asynchronously
  • hx-target to update the content area
  • hx-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']);
   }
}

Wrap Up

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?

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.

Last updated 3 weeks ago.

driesvints, massimoselvi, mshaf liked this article

3
Like this article? Let the author know and give them a clap!
mshaf (Muhammad Shafeeq) Writing at muhammadshafeeq.com

Other articles you might like

Article Hero Image November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
Article Hero Image November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
Article Hero Image November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.