Editar y Eliminar Tareas
Con la lista y creacion funcionando, agregamos las operaciones restantes: toggle de completado, edicion inline del titulo y eliminacion con confirmacion. Cada accion usa un verbo HTTP diferente y HTMX se encarga de actualizar el DOM.
Toggle Completado
El checkbox de cada todo ya tiene hx-put configurado. Solo falta el handler:
func (h *TodoHandler) Toggle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "ID invalido", http.StatusBadRequest)
return
}
todo, ok := h.store.Toggle(id)
if !ok {
http.Error(w, "Todo no encontrado", http.StatusNotFound)
return
}
views.TodoItem(todo).Render(r.Context(), w)
}
Cuando el usuario marca/desmarca el checkbox:
- HTMX envia
PUT /todos/{id}/toggle - El handler invierte
Completedy devuelve el item actualizado hx-swap="outerHTML"reemplaza todo eldiv.todo-item
El item se re-renderiza completo, incluyendo la clase completed que aplica el estilo tachado.
Estilos para Completados
La clase completed se agrega condicionalmente en el TodoItem (capitulo anterior). El CSS correspondiente:
.todo-item.completed .todo-title {
text-decoration: line-through;
opacity: 0.6;
}
Edicion Inline
La edicion inline convierte el titulo en un input al hacer click en “Editar”. Necesitamos un nuevo componente Templ para el modo edicion.
Componente de Edicion
package views
import (
"fmt"
"todo-app/models"
)
templ TodoEditForm(todo models.Todo) {
<div class="todo-item editing"
id={ fmt.Sprintf("todo-%d", todo.ID) }>
<form hx-put={ fmt.Sprintf("/todos/%d", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML">
<input type="text"
name="title"
value={ todo.Title }
class="edit-input"
autofocus
onkeydown="if(event.key==='Escape') htmx.ajax('GET', this.closest('form').getAttribute('hx-cancel'), {target: this.closest('.todo-item'), swap: 'outerHTML'})"
/>
<button type="submit" class="btn-save">Guardar</button>
<button type="button"
class="btn-cancel"
hx-get={ fmt.Sprintf("/todos/%d/view", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML">
Cancelar
</button>
</form>
</div>
}
El formulario de edicion tiene tres elementos clave:
- Un input con el titulo actual como valor
autofocuspara que el cursor entre al input inmediatamente- Boton “Cancelar” que vuelve a la vista normal del todo
Cancelar con Escape
La tecla Escape dispara la misma accion que el boton “Cancelar”: un GET que devuelve la vista normal del item. Usamos onkeydown con un poco de JavaScript inline que delega a HTMX.
Una alternativa mas limpia es usar hx-trigger:
<input type="text"
name="title"
value={ todo.Title }
class="edit-input"
autofocus
hx-get={ fmt.Sprintf("/todos/%d/view", todo.ID) }
hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
hx-swap="outerHTML"
hx-trigger="keyup[key=='Escape']"/>
Asi el input escucha la tecla Escape directamente con HTMX, sin JavaScript manual.
Handler GET /todos/{id}/edit
Devuelve el formulario de edicion:
func (h *TodoHandler) EditForm(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "ID invalido", http.StatusBadRequest)
return
}
todo, ok := h.store.GetByID(id)
if !ok {
http.Error(w, "Todo no encontrado", http.StatusNotFound)
return
}
views.TodoEditForm(todo).Render(r.Context(), w)
}
Handler GET /todos/{id}/view
Devuelve la vista normal (para cancelar la edicion):
func (h *TodoHandler) View(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "ID invalido", http.StatusBadRequest)
return
}
todo, ok := h.store.GetByID(id)
if !ok {
http.Error(w, "Todo no encontrado", http.StatusNotFound)
return
}
views.TodoItem(todo).Render(r.Context(), w)
}
Handler PUT /todos/{id} - Actualizar Titulo
func (h *TodoHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "ID invalido", http.StatusBadRequest)
return
}
title := strings.TrimSpace(r.FormValue("title"))
if title == "" {
http.Error(w, "El titulo es requerido", http.StatusBadRequest)
return
}
todo, ok := h.store.Update(id, title)
if !ok {
http.Error(w, "Todo no encontrado", http.StatusNotFound)
return
}
views.TodoItem(todo).Render(r.Context(), w)
}
Eliminar Tareas
El boton de eliminar ya tiene los atributos HTMX configurados en el TodoItem. El handler:
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "ID invalido", http.StatusBadRequest)
return
}
if !h.store.Delete(id) {
http.Error(w, "Todo no encontrado", http.StatusNotFound)
return
}
// Respuesta vacia: HTMX reemplaza el item con nada
w.WriteHeader(http.StatusOK)
}
El truco esta en devolver una respuesta vacia con status 200. Como el TodoItem tiene hx-swap="outerHTML", HTMX reemplaza el div completo con… nada. El item desaparece del DOM.
Confirmacion antes de Eliminar
El atributo hx-confirm en el boton de eliminar muestra un confirm() nativo del navegador:
<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>
Si el usuario cancela, HTMX no envia el request.
Animacion al Eliminar
Para que el item no desaparezca abruptamente, usamos CSS transitions con las clases de HTMX:
.todo-item {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.todo-item.htmx-swapping {
opacity: 0;
transform: translateX(-20px);
}
HTMX agrega la clase htmx-swapping justo antes de hacer el swap. La transicion CSS se ejecuta y luego el elemento se remueve.
Para que la transicion tenga tiempo de ejecutarse, configura el swap delay:
hx-swap="outerHTML swap:300ms"
Nuevas Rutas en main.go
Agregamos las rutas de edicion y vista:
mux.HandleFunc("GET /todos/{id}/edit", h.EditForm)
mux.HandleFunc("GET /todos/{id}/view", h.View)
mux.HandleFunc("PUT /todos/{id}/toggle", h.Toggle)
mux.HandleFunc("PUT /todos/{id}", h.Update)
mux.HandleFunc("DELETE /todos/{id}", h.Delete)
Flujo de Edicion Completo
- Usuario hace click en “Editar”
- HTMX envia
GET /todos/{id}/edit - El servidor devuelve el formulario de edicion
- El titulo se convierte en un input editable
- El usuario modifica y presiona “Guardar” (o Enter)
- HTMX envia
PUT /todos/{id}con el nuevo titulo - El servidor devuelve el item actualizado en vista normal
Si presiona Escape o “Cancelar”, HTMX pide GET /todos/{id}/view y restaura la vista original.
Resumen
| Operacion | Verbo | Ruta | Swap |
|---|---|---|---|
| Toggle | PUT | /todos/{id}/toggle | outerHTML |
| Modo edicion | GET | /todos/{id}/edit | outerHTML |
| Cancelar edicion | GET | /todos/{id}/view | outerHTML |
| Guardar edicion | PUT | /todos/{id} | outerHTML |
| Eliminar | DELETE | /todos/{id} | outerHTML (vacio) |
El patron es siempre el mismo: un atributo HTMX dispara un request, el servidor responde con HTML parcial y HTMX reemplaza el fragmento en el DOM.
<— Capitulo 10: Listar y Crear Tareas | Capitulo 12: Filtros y Busqueda —>