From 72741c90279c6500fbc19fa3d1f7bce70507d8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Fri, 12 Dec 2025 02:56:51 +0100 Subject: [PATCH] add htmx-alpine hybrid optimistic ui --- .gitignore | 1 + htmx-alpine-pt2/go.mod | 9 + htmx-alpine-pt2/go.sum | 2 + htmx-alpine-pt2/main.go | 223 ++++++++++++++++++ .../templates/fragments/inspector.html | 14 ++ .../templates/fragments/todo-row-form.html | 43 ++++ .../templates/fragments/todo-row.html | 72 ++++++ .../templates/fragments/todo-widget.html | 26 ++ htmx-alpine-pt2/templates/layouts/base.html | 28 +++ htmx-alpine-pt2/templates/pages/index.html | 3 + 10 files changed, 421 insertions(+) create mode 100644 .gitignore create mode 100644 htmx-alpine-pt2/go.mod create mode 100644 htmx-alpine-pt2/go.sum create mode 100644 htmx-alpine-pt2/main.go create mode 100644 htmx-alpine-pt2/templates/fragments/inspector.html create mode 100644 htmx-alpine-pt2/templates/fragments/todo-row-form.html create mode 100644 htmx-alpine-pt2/templates/fragments/todo-row.html create mode 100644 htmx-alpine-pt2/templates/fragments/todo-widget.html create mode 100644 htmx-alpine-pt2/templates/layouts/base.html create mode 100644 htmx-alpine-pt2/templates/pages/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/htmx-alpine-pt2/go.mod b/htmx-alpine-pt2/go.mod new file mode 100644 index 0000000..7c97b09 --- /dev/null +++ b/htmx-alpine-pt2/go.mod @@ -0,0 +1,9 @@ +module htmx + +go 1.25.2 + +require github.com/zepyrshut/hrender v0.0.0-20251204145920-50fdd9cb5ff1 + +require github.com/alexedwards/flow v1.1.0 + +replace github.com/zepyrshut/hrender => ../../hrender diff --git a/htmx-alpine-pt2/go.sum b/htmx-alpine-pt2/go.sum new file mode 100644 index 0000000..881685d --- /dev/null +++ b/htmx-alpine-pt2/go.sum @@ -0,0 +1,2 @@ +github.com/alexedwards/flow v1.1.0 h1:4Xmg4lehS/iI9y6h5Mfm6QSeXdfPdzaTzSKN4RjAATY= +github.com/alexedwards/flow v1.1.0/go.mod h1:DwbobKI6HQD1iMu4/wRgtD4WbmISV8KM3owR9KSSsOQ= diff --git a/htmx-alpine-pt2/main.go b/htmx-alpine-pt2/main.go new file mode 100644 index 0000000..e363da6 --- /dev/null +++ b/htmx-alpine-pt2/main.go @@ -0,0 +1,223 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "sort" + "strconv" + "time" + + "github.com/alexedwards/flow" + "github.com/zepyrshut/hrender" + "github.com/zepyrshut/hrender/ui" +) + +type TodoList struct { + ID int + Name string + Completed bool + + ui.CRUDActions +} + +var todoList = []TodoList{ + { + ID: 1, + Name: "Sacar a la perra", + Completed: true, + }, + { + ID: 2, + Name: "Limpiar la casa", + Completed: false, + }, + { + ID: 3, + Name: "Ir al supermercado", + Completed: false, + }, +} + +func (t *TodoList) GetID() string { + return strconv.Itoa(t.ID) +} + +func (t *TodoList) SetCRUDActions(actions ui.CRUDActions) { + t.CRUDActions = actions +} + +func main() { + templatesFS := os.DirFS("./templates") + + h := hrender.NewHTMLRender(templatesFS, false) + + mux := flow.New() + + mux.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("sleeping 0.5 second, path %s, method %s", r.URL.Path, r.Method) + time.Sleep(200 * time.Millisecond) + next.ServeHTTP(w, r) + }) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8) + err := h.RenderW(w, "pages/index", hrender.H{}, "layouts/base") + if err != nil { + http.Error(w, fmt.Sprintf("error loading template, err: %v", err), http.StatusInternalServerError) + } + }, "GET") + + mux.HandleFunc("/todo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8) + + sort.Slice(todoList, func(i, j int) bool { + return todoList[i].ID > todoList[j].ID + }) + + err := h.RenderW(w, "fragments/todo-widget", hrender.H{ + "Todo": todoList, + }) + if err != nil { + http.Error(w, fmt.Sprintf("error loading template, err: %v", err), http.StatusInternalServerError) + } + }, "GET") + + mux.HandleFunc("/todo", func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8) + + name := r.FormValue("name") + if name == "" { + w.Header().Set("HX-Retarget", "#create-todo-form") + w.Header().Set("HX-Reswap", "outerHTML") + err := h.RenderW(w, "fragments/todo-row-form", hrender.H{ + "Name": name, + "Error": "El nombre es obligatorio", + }) + if err != nil { + http.Error(w, "error loading template", http.StatusInternalServerError) + } + return + } + + newID := 0 + sort.Slice(todoList, func(i, j int) bool { + return todoList[i].ID > todoList[j].ID + }) + if len(todoList) > 0 { + newID = todoList[0].ID + 1 + } else { + newID = 1 + } + + newItem := TodoList{ + ID: newID, + Name: name, + Completed: false, + } + + todoList = append(todoList, newItem) + + err = h.RenderW(w, "fragments/todo-row", hrender.H{ + "ID": newID, + "Name": newItem.Name, + "Completed": false, + }) + if err != nil { + http.Error(w, "error loading template", http.StatusInternalServerError) + } + }, "POST") + + mux.HandleFunc("/todo/:id/completed", func(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + var foundItem *TodoList + for i := range todoList { + if todoList[i].ID == id { + todoList[i].Completed = !todoList[i].Completed + foundItem = &todoList[i] + break + } + } + + if foundItem == nil { + http.Error(w, "item not found", http.StatusNotFound) + return + } + + w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8) + err = h.RenderW(w, "fragments/todo-row", hrender.H{ + "ID": foundItem.ID, + "Name": foundItem.Name, + "Completed": foundItem.Completed, + }) + if err != nil { + http.Error(w, "error loading template", http.StatusInternalServerError) + } + }, "PATCH") + + mux.HandleFunc("/todo/:id", func(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + newName := r.FormValue("name") + if newName == "some error" { + log.Println("error triggered") + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + var foundItem *TodoList + for i := range todoList { + if todoList[i].ID == id { + todoList[i].Name = newName + foundItem = &todoList[i] + break + } + } + + if foundItem == nil { + http.Error(w, "item not found", http.StatusNotFound) + return + } + + w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8) + err = h.RenderW(w, "fragments/todo-row", hrender.H{ + "ID": foundItem.ID, + "Name": foundItem.Name, + "Completed": foundItem.Completed, + }) + if err != nil { + http.Error(w, "error loading template", http.StatusInternalServerError) + } + }, "PUT") + + log.Println("server started on port 8080") + err := http.ListenAndServe(":8080", mux) + if err != nil { + panic("server cannot start") + } +} diff --git a/htmx-alpine-pt2/templates/fragments/inspector.html b/htmx-alpine-pt2/templates/fragments/inspector.html new file mode 100644 index 0000000..61d401d --- /dev/null +++ b/htmx-alpine-pt2/templates/fragments/inspector.html @@ -0,0 +1,14 @@ +{{ $data := .Data }} {{ $depth := .Depth }} {{ $mapa := $data | toMap }} {{ +$depth | indent }} {{ if $mapa }} {{ $nextDepth := add $depth 1 }} + + + +{{ else }} {{ $data }} {{ end }} diff --git a/htmx-alpine-pt2/templates/fragments/todo-row-form.html b/htmx-alpine-pt2/templates/fragments/todo-row-form.html new file mode 100644 index 0000000..ff0e47b --- /dev/null +++ b/htmx-alpine-pt2/templates/fragments/todo-row-form.html @@ -0,0 +1,43 @@ +
+
+ + {{ if .Error }} +

{{ .Error }}

+ {{ end }} +
+ + + + +
diff --git a/htmx-alpine-pt2/templates/fragments/todo-row.html b/htmx-alpine-pt2/templates/fragments/todo-row.html new file mode 100644 index 0000000..25f5f86 --- /dev/null +++ b/htmx-alpine-pt2/templates/fragments/todo-row.html @@ -0,0 +1,72 @@ +
  • +
    +
    + {{ .Name }} +
    + +
    + {{if not .Completed}} + + + {{else}} + Completado + {{ end }} +
    +
    + + +
  • diff --git a/htmx-alpine-pt2/templates/fragments/todo-widget.html b/htmx-alpine-pt2/templates/fragments/todo-widget.html new file mode 100644 index 0000000..2a3ab75 --- /dev/null +++ b/htmx-alpine-pt2/templates/fragments/todo-widget.html @@ -0,0 +1,26 @@ +
    +
    +
    +

    Listado de tareas

    + + +
    + +
    + {{ template "fragments/todo-row-form" .}} +
    + +
      + {{ range .Todo }} {{ template "fragments/todo-row" . }} {{ end }} +
    +
    +
    + +{{ template "fragments/inspector" (dict "Data" . "Depth" 0) }} diff --git a/htmx-alpine-pt2/templates/layouts/base.html b/htmx-alpine-pt2/templates/layouts/base.html new file mode 100644 index 0000000..2e2d1af --- /dev/null +++ b/htmx-alpine-pt2/templates/layouts/base.html @@ -0,0 +1,28 @@ + + + + + + Lista de tareas + + + + + + + + + {{ embed }} + + diff --git a/htmx-alpine-pt2/templates/pages/index.html b/htmx-alpine-pt2/templates/pages/index.html new file mode 100644 index 0000000..e410a7f --- /dev/null +++ b/htmx-alpine-pt2/templates/pages/index.html @@ -0,0 +1,3 @@ +
    +
    Cargando...
    +