diff --git a/examples/htmx-fiber-todo/go.mod b/examples/htmx-fiber-todo/go.mod new file mode 100644 index 0000000..d468daa --- /dev/null +++ b/examples/htmx-fiber-todo/go.mod @@ -0,0 +1,21 @@ +module htmx-fiber-todo + +go 1.21.1 + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/chasefleming/elem-go v0.4.0 // indirect + github.com/gofiber/fiber/v2 v2.50.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) + +replace github.com/chasefleming/elem-go => ../../../elem-go \ No newline at end of file diff --git a/examples/htmx-fiber-todo/go.sum b/examples/htmx-fiber-todo/go.sum new file mode 100644 index 0000000..7f48130 --- /dev/null +++ b/examples/htmx-fiber-todo/go.sum @@ -0,0 +1,29 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/chasefleming/elem-go v0.4.0 h1:SjfVfITUpayyQR/q0e15vD/rik3oQ+lQU8YXixn/M24= +github.com/chasefleming/elem-go v0.4.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4= +github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= +github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/htmx-fiber-todo/main.go b/examples/htmx-fiber-todo/main.go new file mode 100644 index 0000000..f3b768e --- /dev/null +++ b/examples/htmx-fiber-todo/main.go @@ -0,0 +1,175 @@ +package main + +import ( + "github.com/chasefleming/elem-go/htmx" + "strconv" + + "github.com/chasefleming/elem-go" + "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" + "github.com/gofiber/fiber/v2" +) + +// Todo model +type Todo struct { + ID int + Title string + Done bool +} + +// Global todos slice (for simplicity) +var todos = []Todo{ + {ID: 1, Title: "First task", Done: false}, + {ID: 2, Title: "Second task", Done: true}, +} + +func main() { + app := fiber.New() + + // Routes + app.Get("/", renderTodosRoute) + app.Post("/toggle/:id", toggleTodoRoute) + app.Post("/add", addTodoRoute) + + app.Listen(":3000") +} + +func renderTodosRoute(c *fiber.Ctx) error { + c.Type("html") + return c.SendString(renderTodos(todos)) +} + +func toggleTodoRoute(c *fiber.Ctx) error { + id, _ := strconv.Atoi(c.Params("id")) + var updatedTodo Todo + for i, todo := range todos { + if todo.ID == id { + todos[i].Done = !todo.Done + updatedTodo = todos[i] + break + } + } + c.Type("html") + return c.SendString(renderSingleTodo(updatedTodo)) +} + +func addTodoRoute(c *fiber.Ctx) error { + newTitle := c.FormValue("newTodo") + if newTitle != "" { + todos = append(todos, Todo{ID: len(todos) + 1, Title: newTitle, Done: false}) + } + return c.Redirect("/") +} + +func getTodoAttributes(todo Todo) (elem.Attrs, elem.Attrs) { + checkboxAttributes := elem.Attrs{ + attrs.Type: "checkbox", + htmx.HXPost: "/toggle/" + strconv.Itoa(todo.ID), + htmx.HXTrigger: "change", + htmx.HXIndicator: "#saving-indicator", + } + + textAttributes := elem.Attrs{} + if todo.Done { + checkboxAttributes[attrs.Checked] = "checked" + textAttributes[attrs.Style] = elem.ApplyStyle(elem.Style{ + styles.TextDecoration: "line-through", + }) + } + + return checkboxAttributes, textAttributes +} + +func renderSingleTodo(todo Todo) string { + checkboxAttributes, textAttributes := getTodoAttributes(todo) + checkbox := elem.Input(checkboxAttributes) + todoItem := elem.Li(nil, checkbox, elem.Span(textAttributes, elem.Text(todo.Title))) + return todoItem.Render() +} + +func renderTodos(todos []Todo) string { + inputButtonStyle := elem.Style{ + styles.Width: "100%", + styles.Padding: "10px", + styles.MarginBottom: "10px", + styles.Border: "1px solid #ccc", + styles.BorderRadius: "4px", + styles.BackgroundColor: "#f9f9f9", + } + + buttonStyle := elem.Style{ + styles.BackgroundColor: "#007BFF", + styles.Color: "white", + styles.BorderStyle: "none", + styles.BorderRadius: "4px", + styles.Cursor: "pointer", + styles.Width: "100%", + styles.Padding: "8px 12px", + styles.FontSize: "14px", + styles.Height: "36px", + styles.MarginRight: "10px", + } + + listContainerStyle := elem.Style{ + styles.ListStyleType: "none", + styles.Padding: "0", + styles.Width: "100%", + } + + centerContainerStyle := elem.Style{ + styles.MaxWidth: "300px", + styles.Margin: "40px auto", + styles.Padding: "20px", + styles.Border: "1px solid #ccc", + styles.BoxShadow: "0px 0px 10px rgba(0,0,0,0.1)", + styles.BackgroundColor: "#f9f9f9", + } + + headContent := elem.Head(nil, + elem.Script(elem.Attrs{attrs.Src: "https://unpkg.com/htmx.org"}), + ) + bodyContent := elem.Div( + elem.Attrs{attrs.Style: elem.ApplyStyle(centerContainerStyle)}, + elem.H1(nil, elem.Text("Todo List")), + elem.Form( + elem.Attrs{attrs.Method: "post", attrs.Action: "/add"}, + elem.Input( + elem.Attrs{ + attrs.Type: "text", + attrs.Name: "newTodo", + attrs.Placeholder: "Add new task...", + attrs.Style: elem.ApplyStyle(inputButtonStyle), + }, + ), + elem.Button( + elem.Attrs{ + attrs.Type: "submit", + attrs.Style: elem.ApplyStyle(buttonStyle), + }, + elem.Text("Add"), + ), + ), + elem.Ul( + elem.Attrs{attrs.Style: elem.ApplyStyle(listContainerStyle)}, + renderTodoItems(todos)..., + ), + elem.Span( + elem.Attrs{ + attrs.ID: "saving-indicator", + attrs.Style: elem.ApplyStyle(elem.Style{styles.Display: "none"}), + }, + elem.Text("Saving..."), + ), + ) + htmlContent := elem.Html(nil, headContent, bodyContent) + + return htmlContent.Render() +} + +func renderTodoItems(todos []Todo) []elem.Node { + return elem.TransformEach(todos, func(todo Todo) elem.Node { + checkboxAttributes, textAttributes := getTodoAttributes(todo) + checkbox := elem.Input(checkboxAttributes) + return elem.Li(nil, checkbox, elem.Span(textAttributes, elem.Text(todo.Title))) + }) +}