Skip to main content

HTMX Integration Guide

This guide covers HTMX integration patterns used in Gojang to build dynamic, modern web applications without heavy JavaScript frameworks.

Overview​

HTMX allows you to access modern browser features directly from HTML, using attributes instead of JavaScript. In Gojang, HTMX is used to:

  • Submit forms asynchronously
  • Load content dynamically
  • Update parts of the page without full reloads
  • Create modal dialogs
  • Implement CRUD operations seamlessly

Key Benefits:

  • 📦 No build step required
  • 🎯 Progressive enhancement
  • 🚀 Fast, responsive interactions
  • 🔧 Server-side rendering
  • ✨ Clean, maintainable code

Getting Started​

HTMX Setup​

HTMX is already included in base.html:

<script src="https://unpkg.com/htmx.org@1.9.10"></script>

Every page automatically inherits HTMX capabilities through the base template.

CSRF Token Integration​

HTMX requests automatically include CSRF tokens (configured in base.html):

document.addEventListener('htmx:configRequest', function(evt) {
const token = document.querySelector('meta[name="csrf-token"]');
if (token) {
evt.detail.headers['X-CSRF-Token'] = token.content;
}
});

You don't need to configure this yourself - it's already set up! ✅


Core HTMX Attributes​

Basic Attributes​

AttributePurposeExample
hx-getMake a GET requesthx-get="/posts/new"
hx-postMake a POST requesthx-post="/posts"
hx-putMake a PUT requesthx-put="/posts/123"
hx-deleteMake a DELETE requesthx-delete="/posts/123"
hx-targetWhere to put the responsehx-target="#modal"
hx-swapHow to swap the contenthx-swap="innerHTML"

Targeting & Swapping​

hx-target - Specifies where the response goes:

<!-- Replace content inside #modal -->
<button hx-get="/posts/new" hx-target="#modal">

<!-- Replace the button itself -->
<button hx-delete="/posts/1" hx-target="this">

hx-swap - Controls how content is inserted:

ValueBehavior
innerHTMLReplace inner content (default)
outerHTMLReplace entire element
afterbeginInsert before first child
beforeendInsert after last child
beforebeginInsert before element
afterendInsert after element
noneDon't swap, just make request

Pattern 1: Modal Dialogs​

The most common pattern in Gojang - used for forms, confirmations, and detail views.

Template Setup​

Create a modal container in your page:

{{define "content"}}
<div class="container">
<!-- Your page content -->
<button hx-get="/posts/new" hx-target="#modal" hx-swap="innerHTML" class="btn btn-primary">
+ New Post
</button>
</div>

<!-- Modal container (empty until triggered) -->
<div id="modal" class="modal"></div>
{{end}}

Create the modal form (e.g., posts/new.partial.html):

<div class="modal-backdrop" onclick="this.parentElement.innerHTML = ''">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>Create New Post</h3>
<button onclick="this.closest('.modal-backdrop').parentElement.innerHTML = ''"
class="modal-close">&times;</button>
</div>

<form hx-post="/posts"
hx-target=".posts-container"
hx-swap="afterbegin"
hx-on::after-request="if(event.detail.successful) document.getElementById('modal').innerHTML = ''"
class="form">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

<div class="form-group">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" required>
</div>

<div class="modal-footer">
<button type="button"
onclick="this.closest('.modal-backdrop').parentElement.innerHTML = ''"
class="btn btn-secondary">
Cancel
</button>
<button type="submit" class="btn btn-primary">Create Post</button>
</div>
</form>
</div>
</div>

Handler Pattern​

func (h *PostHandler) New(w http.ResponseWriter, r *http.Request) {
// Simply render the modal template
h.Renderer.Render(w, r, "posts/new.partial.html", nil)
}

func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
// Parse and validate form
if err := r.ParseForm(); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, "Invalid form data")
return
}

// ... validation and save logic ...

// Close modal with HX-Trigger header
w.Header().Set("HX-Trigger", "closeModal")

// Return updated content
h.Renderer.Render(w, r, "posts/list.partial.html", data)
}

Key Points:

  • ✅ Click outside modal (backdrop) to close
  • ✅ Close button in header
  • ✅ Auto-close on successful form submission
  • ✅ Errors keep modal open for correction

Pattern 2: Form Submissions​

Basic Form Submission​

<form hx-post="/posts" hx-target="#posts-list" hx-swap="innerHTML">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

<div class="form-group">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" required>
{{if index .Errors "Subject"}}
<span class="error">{{index .Errors "Subject"}}</span>
{{end}}
</div>

<button type="submit" class="btn btn-primary">Submit</button>
</form>

Handler Response Pattern​

func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
// Parse form
if err := r.ParseForm(); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, "Invalid form data")
return
}

// Validate
errors := forms.Validate(form)
if len(errors) > 0 {
// Re-render form with errors
h.Renderer.Render(w, r, "posts/new.partial.html", &renderers.TemplateData{
Errors: errors,
})
return
}

// Save to database
// ...

// Return success response (partial or full page)
h.Renderer.Render(w, r, "posts/list.partial.html", data)
}

Validation Pattern:

  • ❌ Errors: Re-render form with error messages
  • ✅ Success: Return updated content

Pattern 3: CRUD Operations​

Create - Add New Items​

Button to open form:

<button hx-get="/posts/new" 
hx-target="#modal"
hx-swap="innerHTML"
class="btn btn-primary">
+ New Post
</button>

Form submits to list:

<form hx-post="/posts" 
hx-target=".posts-container"
hx-swap="afterbegin">
<!-- form fields -->
</form>

Handler adds item and returns list:

func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
// ... create post ...

// Close modal
w.Header().Set("HX-Trigger", "closeModal")

// Return updated list
posts, _ := h.Client.Post.Query().All(r.Context())
h.Renderer.Render(w, r, "posts/list.partial.html", &renderers.TemplateData{
Data: map[string]interface{}{"Posts": posts},
})
}

Read/Edit - Update Items​

Edit button on each item:

<button hx-get="/posts/{{.ID}}/edit" 
hx-target="#modal"
hx-swap="innerHTML"
class="btn-sm btn-secondary">
Edit
</button>

Edit form replaces specific item:

<form hx-put="/posts/{{.Data.Post.ID}}" 
hx-target="#post-{{.Data.Post.ID}}"
hx-swap="outerHTML"
hx-on::after-request="if(event.detail.successful) document.getElementById('modal').innerHTML = ''">
<!-- form fields with values -->
</form>

Handler returns updated item card:

func (h *PostHandler) Update(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))

// ... update post ...

// Close modal
w.Header().Set("HX-Trigger", "closeModal")

// Return updated post card
h.Renderer.Render(w, r, "posts/card.partial.html", &renderers.TemplateData{
Data: map[string]interface{}{"Post": updatedPost},
})
}

Delete - Remove Items​

Two approaches:

1. Direct Delete (with confirmation)​

<button hx-delete="/posts/{{.ID}}" 
hx-target="#post-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete this post?"
class="btn-sm btn-danger">
Delete
</button>

Handler returns empty response (element is removed):

func (h *PostHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))

// Delete post
h.Client.Post.DeleteOneID(id).ExecX(r.Context())

// Return nothing (hx-swap="outerHTML" removes the element)
w.WriteHeader(http.StatusOK)
}

2. Confirmation Modal​

<button hx-get="/posts/{{.ID}}/delete" 
hx-target="#modal"
hx-swap="innerHTML"
class="btn-sm btn-danger">
Delete
</button>

Delete confirmation template:

<div class="modal-backdrop">
<div class="modal-content">
<h3>Confirm Delete</h3>
<p>Are you sure you want to delete "{{.Data.Post.Subject}}"?</p>

<div class="modal-footer">
<button onclick="this.closest('.modal-backdrop').parentElement.innerHTML = ''"
class="btn btn-secondary">
Cancel
</button>
<button hx-delete="/posts/{{.Data.Post.ID}}"
hx-target="#post-{{.Data.Post.ID}}"
hx-swap="outerHTML"
class="btn btn-danger">
Delete
</button>
</div>
</div>
</div>

Pattern 4: Partial Templates​

Partial templates are HTML fragments returned by HTMX requests. They don't include the full page structure.

Creating Partials​

File naming convention: Use .partial.html suffix (e.g., posts/list.partial.html):

<!-- posts/list.partial.html -->
{{range .Data.Posts}}
<div class="card post-card" id="post-{{.ID}}">
<h3>{{.Subject}}</h3>
<div class="post-body">{{.Body}}</div>
</div>
{{else}}
<div class="card">
<p>No posts yet.</p>
</div>
{{end}}

Using Partials​

<!-- Full page template -->
<div id="posts-list" class="posts-container">
{{range .Data.Posts}}
<!-- Initial render -->
{{end}}
</div>

<!-- Button that reloads the list -->
<button hx-get="/posts/reload" hx-target="#posts-list" hx-swap="innerHTML">
Refresh Posts
</button>

Handler for Partials​

func (h *PostHandler) Reload(w http.ResponseWriter, r *http.Request) {
posts, _ := h.Client.Post.Query().All(r.Context())

// Return only the partial
h.Renderer.Render(w, r, "posts/list.partial.html", &renderers.TemplateData{
Data: map[string]interface{}{"Posts": posts},
})
}

Pattern 5: Dynamic Content Loading​

Load on Button Click​

<div class="container">
<h1>About Us</h1>

<button hx-get="/about/team"
hx-target="#team-section"
hx-swap="innerHTML"
class="btn btn-primary">
Load Team Members
</button>

<div id="team-section" style="margin-top: 1.5rem;">
<!-- Content loads here -->
</div>
</div>

Handler returns HTML fragment:

func (h *PageHandler) AboutTeam(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
<div class="team-grid">
<div class="card">John Doe - CEO</div>
<div class="card">Jane Smith - CTO</div>
</div>
`))
}

Load on Page Load​

Use hx-trigger="load" to fetch content automatically:

<div hx-get="/dashboard/stats" 
hx-trigger="load"
hx-swap="innerHTML">
Loading stats...
</div>

Lazy Loading Images/Content​

<div hx-get="/posts/{{.ID}}/details" 
hx-trigger="intersect once"
hx-swap="innerHTML">
<div class="loading">Loading...</div>
</div>

Loads content when element scrolls into view (infinite scroll pattern).


Pattern 6: Advanced Swap Strategies​

Prepend New Items​

Add new items to the top of a list:

<form hx-post="/posts" 
hx-target="#posts-list"
hx-swap="afterbegin">

Append to List​

Add items to the bottom:

<button hx-get="/posts/more" 
hx-target="#posts-list"
hx-swap="beforeend">
Load More
</button>

Replace Specific Element​

Update individual items:

<form hx-put="/posts/{{.ID}}" 
hx-target="#post-{{.ID}}"
hx-swap="outerHTML">

Out-of-Band Swaps​

Update multiple page areas from one response:

<!-- In handler response -->
<div id="post-123" hx-swap-oob="true">
<!-- Updated post content -->
</div>
<div id="post-count" hx-swap-oob="true">
<span>Total: 42 posts</span>
</div>

The main target receives normal content, other divs with hx-swap-oob="true" update their matching IDs.


Pattern 7: Event Handling​

Built-in Events​

<!-- Submit on Enter key -->
<input hx-post="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results">

<!-- Submit on change -->
<select hx-get="/filter"
hx-trigger="change"
hx-target="#products">

<!-- Multiple triggers -->
<button hx-get="/data"
hx-trigger="click, every 30s">

Custom Events​

Trigger actions after request completes:

<form hx-post="/posts"
hx-on::after-request="if(event.detail.successful) showSuccessMessage()">

HTMX Events​

<!-- Show loading indicator -->
<form hx-post="/save"
hx-indicator="#spinner">
<button type="submit">Save</button>
</form>
<div id="spinner" class="htmx-indicator">Loading...</div>

Server-Triggered Events​

Send custom headers to trigger client-side events:

// In handler
w.Header().Set("HX-Trigger", "closeModal")
w.Header().Set("HX-Trigger", `{"showMessage": {"text": "Post created!"}}`)

Listen in JavaScript:

document.addEventListener('showMessage', function(evt) {
alert(evt.detail.text);
});

Pattern 8: Response Headers​

HTMX recognizes special response headers to control behavior:

Common Response Headers​

// Close modal after successful action
w.Header().Set("HX-Trigger", "closeModal")

// Change swap target
w.Header().Set("HX-Retarget", "#different-element")

// Change swap strategy
w.Header().Set("HX-Reswap", "innerHTML")

// Client-side redirect
w.Header().Set("HX-Redirect", "/dashboard")

// Refresh the page
w.Header().Set("HX-Refresh", "true")

Example: Dynamic Retargeting​

func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
// ... create post ...

// Change where response goes
w.Header().Set("HX-Retarget", "#posts-list")
w.Header().Set("HX-Reswap", "innerHTML")

// Close modal
w.Header().Set("HX-Trigger", "closeModal")

h.Renderer.Render(w, r, "posts/list.partial.html", data)
}

Authentication & Authorization​

Checking for HTMX Requests​

func (h *Handler) Action(w http.ResponseWriter, r *http.Request) {
// Check if this is an HTMX request
if r.Header.Get("HX-Request") == "true" {
// Return partial
h.Renderer.Render(w, r, "partial.html", data)
} else {
// Return full page
h.Renderer.Render(w, r, "full.html", data)
}
}

Protected Routes​

HTMX works seamlessly with authentication middleware:

// In routes
r.Group(func(r chi.Router) {
r.Use(middleware.RequireAuth(sm, client))

r.Get("/posts/new", handler.New)
r.Post("/posts", handler.Create)
r.Put("/posts/{id}", handler.Update)
r.Delete("/posts/{id}", handler.Delete)
})

If not authenticated, middleware redirects or returns error - HTMX displays it in the target.

Handling Auth Errors in HTMX​

Return proper error templates:

func (h *Handler) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !authenticated {
if r.Header.Get("HX-Request") == "true" {
// Return error fragment
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`<div class="alert alert-error">Please log in</div>`))
return
}
// Regular request - redirect to login
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}

Best Practices​

✅ DO​

  1. Use semantic IDs for targets

    <div id="post-123">  <!-- ✅ Descriptive -->
    <div id="item-1"> <!-- ❌ Generic -->
  2. Include CSRF tokens in all forms

    <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
  3. Use partial templates for HTMX responses

    posts/index.html        # Full page
    posts/list.partial.html # Just the list
    posts/card.partial.html # Single item
  4. Validate on the server - Never trust client input

    errors := forms.Validate(form)
    if len(errors) > 0 {
    h.Renderer.Render(w, r, "form.html", &renderers.TemplateData{
    Errors: errors,
    })
    return
    }
  5. Return appropriate HTTP status codes

    w.WriteHeader(http.StatusBadRequest)  // 400 for validation errors
    w.WriteHeader(http.StatusNotFound) // 404 for missing resources
  6. Use loading indicators for slow operations

    <button hx-post="/save" hx-indicator="#spinner">Save</button>
    <div id="spinner" class="htmx-indicator">Saving...</div>

❌ DON'T​

  1. Don't forget error handling - Always handle form validation errors
  2. Don't skip CSRF protection - It's required for security
  3. Don't return full pages for HTMX requests - Use partials
  4. Don't use inline JavaScript when HTMX attributes work
  5. Don't put business logic in templates - Keep it in handlers

Common Patterns Summary​

<!-- 1. Button opens modal -->
<button hx-get="/posts/new" hx-target="#modal">New</button>

<!-- 2. Modal form submits -->
<form hx-post="/posts" hx-target="#posts-list" hx-swap="innerHTML">

<!-- 3. Handler returns updated list -->

Edit in Modal → Update Specific Item​

<!-- 1. Edit button opens modal -->
<button hx-get="/posts/123/edit" hx-target="#modal">Edit</button>

<!-- 2. Form updates specific item -->
<form hx-put="/posts/123" hx-target="#post-123" hx-swap="outerHTML">

Delete with Confirmation​

<button hx-delete="/posts/123" 
hx-target="#post-123"
hx-swap="outerHTML"
hx-confirm="Delete this post?">
Delete
</button>

Dynamic Content Loading​

<div hx-get="/content" hx-trigger="load" hx-swap="innerHTML">
Loading...
</div>

Troubleshooting​

Content Not Updating​

Problem: HTMX request succeeds but page doesn't update

Solutions:

  • ✅ Check hx-target selector is correct and element exists
  • ✅ Verify handler returns HTML (not JSON or empty response)
  • ✅ Check hx-swap strategy matches your intent
  • ✅ Use browser DevTools Network tab to inspect response

Form Not Submitting​

Problem: Form submit doesn't trigger HTMX request

Solutions:

  • ✅ Ensure HTMX is loaded (check browser console)
  • ✅ Check form has hx-post, hx-put, etc.
  • ✅ Verify CSRF token is included
  • ✅ Check for JavaScript errors in console

Problem: Modal stays open after successful submission

Solutions:

  • ✅ Add hx-on::after-request to form
  • ✅ Set HX-Trigger: closeModal header in handler
  • ✅ Check modal event listener is registered (in base.html)

403 Forbidden Errors​

Problem: HTMX requests return 403

Solutions:

  • ✅ Include CSRF token in form: <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
  • ✅ Check CSRF header configuration in base.html
  • ✅ Ensure session is valid

Partial Returns Full Page​

Problem: HTMX request gets full HTML page instead of fragment

Solutions:

  • ✅ Use correct template name (e.g., posts/list.partial.html)
  • ✅ Don't render with base template for partials
  • ✅ Check handler is using correct template

Complete Example: Todo List​

Here's a complete CRUD example combining all patterns:

Template: todos/index.html​

{{define "title"}}My Todos{{end}}

{{define "content"}}
<div class="container">
<div class="page-header">
<h2>My Todo List</h2>
<button hx-get="/todos/new" hx-target="#modal" hx-swap="innerHTML" class="btn btn-primary">
+ Add Todo
</button>
</div>

<div id="todos-list" class="todos-container">
{{range .Data.Todos}}
<div class="card todo-card" id="todo-{{.ID}}">
<div class="todo-content">
<input type="checkbox"
{{if .Completed}}checked{{end}}
hx-put="/todos/{{.ID}}/toggle"
hx-target="#todo-{{.ID}}"
hx-swap="outerHTML">
<span class="{{if .Completed}}completed{{end}}">{{.Title}}</span>
</div>
<div class="todo-actions">
<button hx-get="/todos/{{.ID}}/edit"
hx-target="#modal"
hx-swap="innerHTML"
class="btn-sm btn-secondary">
Edit
</button>
<button hx-delete="/todos/{{.ID}}"
hx-target="#todo-{{.ID}}"
hx-swap="outerHTML"
hx-confirm="Delete this todo?"
class="btn-sm btn-danger">
Delete
</button>
</div>
</div>
{{else}}
<div class="card">
<p class="text-center">No todos yet. Add one to get started!</p>
</div>
{{end}}
</div>
</div>

<div id="modal" class="modal"></div>
{{end}}

Template: todos/new.partial.html​

<div class="modal-backdrop" onclick="this.parentElement.innerHTML = ''">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>New Todo</h3>
<button onclick="this.closest('.modal-backdrop').parentElement.innerHTML = ''"
class="modal-close">&times;</button>
</div>

<form hx-post="/todos"
hx-target="#todos-list"
hx-swap="innerHTML"
hx-on::after-request="if(event.detail.successful) document.getElementById('modal').innerHTML = ''"
class="form">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
{{if index .Errors "Title"}}
<span class="error">{{index .Errors "Title"}}</span>
{{end}}
</div>

<div class="modal-footer">
<button type="button"
onclick="this.closest('.modal-backdrop').parentElement.innerHTML = ''"
class="btn btn-secondary">
Cancel
</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>

Template: todos/card.partial.html​

<div class="card todo-card" id="todo-{{.Data.Todo.ID}}">
<div class="todo-content">
<input type="checkbox"
{{if .Data.Todo.Completed}}checked{{end}}
hx-put="/todos/{{.Data.Todo.ID}}/toggle"
hx-target="#todo-{{.Data.Todo.ID}}"
hx-swap="outerHTML">
<span class="{{if .Data.Todo.Completed}}completed{{end}}">{{.Data.Todo.Title}}</span>
</div>
<div class="todo-actions">
<button hx-get="/todos/{{.Data.Todo.ID}}/edit"
hx-target="#modal"
hx-swap="innerHTML"
class="btn-sm btn-secondary">
Edit
</button>
<button hx-delete="/todos/{{.Data.Todo.ID}}"
hx-target="#todo-{{.Data.Todo.ID}}"
hx-swap="outerHTML"
hx-confirm="Delete this todo?"
class="btn-sm btn-danger">
Delete
</button>
</div>
</div>

Handler: app/todos/todos.handler.go​

package todos

import (
"net/http"
"strconv"

"github.com/go-chi/chi/v5"
"github.com/gojangframework/gojang/app/gojang/models"
"github.com/gojangframework/gojang/app/gojang/views/renderers"
)

type TodoHandler struct {
Client *models.Client
Renderer *renderers.Renderer
}

// Index - List all todos
func (h *TodoHandler) Index(w http.ResponseWriter, r *http.Request) {
todos, err := h.Client.Todo.Query().All(r.Context())
if err != nil {
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to load todos")
return
}

h.Renderer.Render(w, r, "todos/index.html", &renderers.TemplateData{
Data: map[string]interface{}{"Todos": todos},
})
}

// New - Show create form
func (h *TodoHandler) New(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "todos/new.partial.html", nil)
}

// Create - Create new todo
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, "Invalid form data")
return
}

title := r.Form.Get("title")
if title == "" {
h.Renderer.Render(w, r, "todos/new.partial.html", &renderers.TemplateData{
Errors: map[string]string{"Title": "Title is required"},
})
return
}

// Create todo
_, err := h.Client.Todo.Create().
SetTitle(title).
SetCompleted(false).
Save(r.Context())
if err != nil {
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to create todo")
return
}

// Close modal
w.Header().Set("HX-Trigger", "closeModal")

// Return updated list
todos, _ := h.Client.Todo.Query().All(r.Context())
h.Renderer.Render(w, r, "todos/list.partial.html", &renderers.TemplateData{
Data: map[string]interface{}{"Todos": todos},
})
}

// Toggle - Toggle todo completion
func (h *TodoHandler) Toggle(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))

todo, err := h.Client.Todo.Get(r.Context(), id)
if err != nil {
h.Renderer.RenderError(w, r, http.StatusNotFound, "Todo not found")
return
}

// Toggle completed status
todo, err = h.Client.Todo.UpdateOne(todo).
SetCompleted(!todo.Completed).
Save(r.Context())
if err != nil {
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to update todo")
return
}

// Return updated card
h.Renderer.Render(w, r, "todos/card.partial.html", &renderers.TemplateData{
Data: map[string]interface{}{"Todo": todo},
})
}

// Delete - Delete todo
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))

if err := h.Client.Todo.DeleteOneID(id).Exec(r.Context()); err != nil {
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to delete todo")
return
}

// Return empty (outerHTML swap removes the element)
w.WriteHeader(http.StatusOK)
}

Next Steps​

  • ✅ Practice: Try adding HTMX to your existing pages
  • ✅ Read: HTMX Documentation
  • ✅ Explore: Check app/posts/templates/ for more examples
  • ✅ Build: Create your own CRUD feature using these patterns

Quick Reference​

Essential Attributes​

hx-get="/path"           <!-- GET request -->
hx-post="/path" <!-- POST request -->
hx-put="/path" <!-- PUT request -->
hx-delete="/path" <!-- DELETE request -->
hx-target="#id" <!-- Where response goes -->
hx-swap="innerHTML" <!-- How to insert response -->
hx-trigger="click" <!-- When to trigger -->
hx-confirm="message" <!-- Confirmation dialog -->

Common Patterns​

PatternAttributes
Modal Formhx-get="/form" hx-target="#modal" hx-swap="innerHTML"
Create Itemhx-post="/items" hx-target="#list" hx-swap="afterbegin"
Update Itemhx-put="/items/1" hx-target="#item-1" hx-swap="outerHTML"
Delete Itemhx-delete="/items/1" hx-target="#item-1" hx-swap="outerHTML"
Load on Page Loadhx-get="/data" hx-trigger="load"
Search with Delayhx-get="/search" hx-trigger="keyup changed delay:500ms"

Happy coding with HTMX! 🚀