Listar y Crear Tareas
Con la estructura del proyecto lista, implementamos las dos operaciones fundamentales: listar todos los todos y crear nuevos. Aqui es donde HTMX brilla: el formulario agrega tareas sin recargar la pagina.
Handler GET / - Pagina Principal
El handler Index renderiza la pagina completa con el layout, el formulario y la lista inicial:
func (h *TodoHandler) Index(w http.ResponseWriter, r *http.Request) {
todos := h.store.GetAll()
views.IndexPage(todos).Render(r.Context(), w)
}
Vista Principal
Creamos views/index.templ con la pagina completa:
package views
import "todo-app/models"
templ IndexPage(todos []models.Todo) {
@Layout("Todo List") {
@TodoForm()
<div id="todo-list">
@TodoList(todos)
</div>
<div id="todo-count">
@TodoCount(countPending(todos))
</div>
}
}
La pagina compone tres componentes: formulario, lista y contador. Cada uno es independiente y reemplazable por HTMX.
Formulario de Creacion
El formulario usa hx-post para enviar datos sin JavaScript:
package views
templ TodoForm() {
<form hx-post="/todos"
hx-target="#todo-list"
hx-swap="beforeend"
hx-on::after-request="this.reset()">
<div class="todo-form">
<input type="text"
name="title"
placeholder="Nueva tarea..."
required
autocomplete="off"/>
<button type="submit">Agregar</button>
</div>
</form>
}
Desglose de atributos HTMX:
hx-post="/todos": envia POST al crearhx-target="#todo-list": donde insertar la respuestahx-swap="beforeend": agrega al final de la lista (no reemplaza)hx-on::after-request="this.reset()": limpia el input despues de enviar
El this.reset() es un truco simple: al completar el request, resetea el formulario para que el input quede vacio y listo para la siguiente tarea.
Componente Lista de Todos
La lista itera sobre los todos y renderiza cada uno:
package views
import "todo-app/models"
templ TodoList(todos []models.Todo) {
if len(todos) == 0 {
@EmptyState()
}
for _, todo := range todos {
@TodoItem(todo)
}
}
Si no hay todos, mostramos un estado vacio.
Componente Todo Individual
Cada todo es un componente con checkbox, titulo y botones de accion:
package views
import (
"fmt"
"todo-app/models"
)
templ TodoItem(todo models.Todo) {
<div class={ "todo-item", templ.KV("completed", todo.Completed) }
id={ fmt.Sprintf("todo-%d", todo.ID) }>
<input type="checkbox"
if todo.Completed {
checked
}
hx-put={ fmt.Sprintf("/todos/%d/toggle", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML"/>
<span class="todo-title">{ todo.Title }</span>
<div class="todo-actions">
<button class="btn-edit"
hx-get={ fmt.Sprintf("/todos/%d/edit", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML">
Editar
</button>
<button class="btn-delete"
hx-delete={ fmt.Sprintf("/todos/%d", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML"
hx-confirm="Eliminar esta tarea?">
X
</button>
</div>
</div>
}
Puntos importantes:
- El
idunico (todo-{id}) permite que HTMX apunte a cada item templ.KV("completed", todo.Completed)agrega la clase CSS condicionalmente- El checkbox hace toggle con
hx-puty reemplaza todo el item - Cada boton apunta al
#todo-{id}de su propio item
Estado Vacio
Cuando no hay tareas mostramos un mensaje:
package views
templ EmptyState() {
<div class="empty-state" id="empty-state">
<p>No hay tareas. Agrega una arriba.</p>
</div>
}
Handler POST /todos - Crear Tarea
El handler de creacion extrae el titulo del formulario, crea el todo y devuelve solo el HTML del nuevo item:
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
title := strings.TrimSpace(r.FormValue("title"))
if title == "" {
http.Error(w, "El titulo es requerido", http.StatusBadRequest)
return
}
todo := h.store.Add(title)
views.TodoItem(todo).Render(r.Context(), w)
}
Esto es clave: el servidor no devuelve la pagina completa, solo el fragmento HTML del nuevo todo. HTMX lo inserta en el #todo-list gracias a hx-swap="beforeend".
Handler GET /todos - Listar
Este handler devuelve la lista completa, util para cuando se aplican filtros:
func (h *TodoHandler) List(w http.ResponseWriter, r *http.Request) {
todos := h.store.GetAll()
views.TodoList(todos).Render(r.Context(), w)
}
Contador de Tareas Pendientes
Un helper cuenta las tareas no completadas:
package views
import (
"fmt"
"todo-app/models"
)
func countPending(todos []models.Todo) int {
count := 0
for _, t := range todos {
if !t.Completed {
count++
}
}
return count
}
templ TodoCount(pending int) {
<p class="todo-count">
{ fmt.Sprintf("%d", pending) } tareas pendientes
</p>
}
Imports Necesarios en el Handler
El archivo completo de handlers/todo.go hasta este punto:
package handlers
import (
"net/http"
"strings"
"todo-app/models"
"todo-app/views"
)
type TodoHandler struct {
store *models.TodoStore
}
func NewTodoHandler(store *models.TodoStore) *TodoHandler {
return &TodoHandler{store: store}
}
func (h *TodoHandler) Index(w http.ResponseWriter, r *http.Request) {
todos := h.store.GetAll()
views.IndexPage(todos).Render(r.Context(), w)
}
func (h *TodoHandler) List(w http.ResponseWriter, r *http.Request) {
todos := h.store.GetAll()
views.TodoList(todos).Render(r.Context(), w)
}
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
title := strings.TrimSpace(r.FormValue("title"))
if title == "" {
http.Error(w, "El titulo es requerido", http.StatusBadRequest)
return
}
todo := h.store.Add(title)
views.TodoItem(todo).Render(r.Context(), w)
}
Flujo Completo
- El usuario abre
/y ve la pagina con el formulario y la lista - Escribe una tarea y presiona “Agregar”
- HTMX envia
POST /todoscon el titulo - El servidor crea el todo y devuelve el HTML del item
- HTMX inserta el HTML al final de
#todo-list - El formulario se limpia automaticamente
Todo sin una sola linea de JavaScript manual. HTMX maneja el request, la insercion y el swap.
Probar la App
templ generate
go run .
Abre http://localhost:8080 y prueba agregar tareas. Deberias ver como aparecen en la lista sin que la pagina se recargue.
Resumen
| Operacion | Ruta | HTMX | Resultado |
|---|---|---|---|
| Ver pagina | GET / | - | Pagina completa |
| Listar | GET /todos | - | Lista de items |
| Crear | POST /todos | hx-post, beforeend | Nuevo item al final |
<— Capitulo 9: Estructura del Proyecto | Capitulo 11: Editar y Eliminar Tareas —>