add htmx-alpine with optimistic ui experience
This commit is contained in:
parent
ddc3d77394
commit
cfa0734f08
5
htmx-alpine/go.mod
Normal file
5
htmx-alpine/go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module htmx
|
||||||
|
|
||||||
|
go 1.25.2
|
||||||
|
|
||||||
|
require github.com/zepyrshut/hrender v0.0.0-20251204145920-50fdd9cb5ff1
|
||||||
4
htmx-alpine/go.sum
Normal file
4
htmx-alpine/go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
github.com/zepyrshut/hrender v0.0.0-20251204134824-5eb5dc8eaf21 h1:jUfJj+Ymdd9krHUt3YzBxR5kFoRAp+RD3u4Yir/KSGQ=
|
||||||
|
github.com/zepyrshut/hrender v0.0.0-20251204134824-5eb5dc8eaf21/go.mod h1:KxR0Cisj52sFFxMTm3o+OJWBqP/khUpA1bjjc49iAiM=
|
||||||
|
github.com/zepyrshut/hrender v0.0.0-20251204145920-50fdd9cb5ff1 h1:Zpay8/pWw++3B/QXsGbF3eTI1z2tGynguQWAnERIg9c=
|
||||||
|
github.com/zepyrshut/hrender v0.0.0-20251204145920-50fdd9cb5ff1/go.mod h1:KxR0Cisj52sFFxMTm3o+OJWBqP/khUpA1bjjc49iAiM=
|
||||||
196
htmx-alpine/main.go
Normal file
196
htmx-alpine/main.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zepyrshut/hrender"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TodoList struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Completed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var todoList = []TodoList{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Sacar a la perra",
|
||||||
|
Completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Limpiar la casa",
|
||||||
|
Completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Name: "Ir al supermercado",
|
||||||
|
Completed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
templatesFS := os.DirFS("./templates")
|
||||||
|
|
||||||
|
h := hrender.NewHTMLRender(templatesFS, false)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8)
|
||||||
|
err := h.Render(w, "pages/index", hrender.H{})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error loading template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("GET /todo", http.HandlerFunc(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.Render(w, "fragments/todo-widget", hrender.H{
|
||||||
|
"TodoList": todoList,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error loading template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("GET /todo/new", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8)
|
||||||
|
err := h.Render(w, "fragments/todo-row-edit", hrender.H{})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error loading template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("POST /todo", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newID := 0
|
||||||
|
sort.Slice(todoList, func(i, j int) bool {
|
||||||
|
return todoList[i].ID > todoList[j].ID
|
||||||
|
})
|
||||||
|
newID = todoList[0].ID + 1
|
||||||
|
|
||||||
|
log.Println(todoList[len(todoList)-1].ID)
|
||||||
|
log.Println(todoList)
|
||||||
|
|
||||||
|
newItem := TodoList{
|
||||||
|
ID: newID,
|
||||||
|
Name: r.FormValue("name"),
|
||||||
|
Completed: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
todoList = append(todoList, newItem)
|
||||||
|
|
||||||
|
w.Header().Set(hrender.ContentType, hrender.ContentTextHTMLUTF8)
|
||||||
|
err = h.Render(w, "fragments/todo-row", hrender.H{
|
||||||
|
"ID": newID,
|
||||||
|
"Name": newItem.Name,
|
||||||
|
"Completed": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error loading template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("PATCH /todo/{id}/completed", http.HandlerFunc(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 = true
|
||||||
|
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.Render(w, "fragments/todo-row", hrender.H{
|
||||||
|
"ID": foundItem.ID,
|
||||||
|
"Name": foundItem.Name,
|
||||||
|
"Completed": true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error loading template", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.Handle("PUT /todo/{id}/update", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("put triggered, sleeping 2 seconds")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
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.Render(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)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
log.Println("server started on port 8080")
|
||||||
|
err := http.ListenAndServe(":8080", mux)
|
||||||
|
if err != nil {
|
||||||
|
panic("server cannot start")
|
||||||
|
}
|
||||||
|
}
|
||||||
44
htmx-alpine/templates/fragments/todo-row.html
Normal file
44
htmx-alpine/templates/fragments/todo-row.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<li
|
||||||
|
id="todo-{{.ID}}"
|
||||||
|
x-data="{ editing: false, name: '{{.Name}}', backup: '{{.Name}}' }"
|
||||||
|
@htmx:response-error="name = backup; $dispatch('show-toast', 'Error al actualizar. Inténtelo de nuevo.');"
|
||||||
|
>
|
||||||
|
<div x-show="!editing" @dblclick="editing = true" x-text="name">
|
||||||
|
{{ .Name }} {{ .Completed }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="editing" style="display: none">
|
||||||
|
<form
|
||||||
|
hx-put="/todo/{{.ID}}/update"
|
||||||
|
hx-target="closest li"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
@submit="editing = false"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value="{{ .Name }}"
|
||||||
|
@keydown.escape="name = backup; editing = false"
|
||||||
|
x-model="name"
|
||||||
|
x-init="$el.focus()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="button" @click="name =backup; editing = false">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit">Confirmar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button x-show="!editing" @click="editing = true">Editar</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
x-show="!editing"
|
||||||
|
hx-patch="/todo/{{.ID}}/completed"
|
||||||
|
hx-target="closest li"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
Completado
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
9
htmx-alpine/templates/fragments/todo-widget.html
Normal file
9
htmx-alpine/templates/fragments/todo-widget.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<h2>Listado de tareas</h2>
|
||||||
|
|
||||||
|
<button hx-get="/todo/new" hx-target="#todo-list-body" hx-swap="afterbegin">
|
||||||
|
Nueva tarea
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ol id="todo-list-body">
|
||||||
|
{{ range .TodoList }} {{ template "fragments/todo-row" . }} {{ end }}
|
||||||
|
</ol>
|
||||||
24
htmx-alpine/templates/pages/index.html
Normal file
24
htmx-alpine/templates/pages/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Lista de tareas</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div hx-get="/todo" hx-trigger="load" hx-swap="outerHTML">Cargando...</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="{ show: false, message: '' }"
|
||||||
|
@show-toast.window="show = true; message = $event.detail; setTimeout(() => show = false, 3000)"
|
||||||
|
x-show="show"
|
||||||
|
>
|
||||||
|
<span x-text="message"></span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user