Listar y Crear Tareas

Por: Artiko
gotemplhtmxproyectotodo-list

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:

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:

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

  1. El usuario abre / y ve la pagina con el formulario y la lista
  2. Escribe una tarea y presiona “Agregar”
  3. HTMX envia POST /todos con el titulo
  4. El servidor crea el todo y devuelve el HTML del item
  5. HTMX inserta el HTML al final de #todo-list
  6. 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

OperacionRutaHTMXResultado
Ver paginaGET /-Pagina completa
ListarGET /todos-Lista de items
CrearPOST /todoshx-post, beforeendNuevo item al final

<— Capitulo 9: Estructura del Proyecto | Capitulo 11: Editar y Eliminar Tareas —>