Skip to content

Commit

Permalink
task: adjusting the html to use gomponents (less dependencies)
Browse files Browse the repository at this point in the history
  • Loading branch information
paganotoni committed Oct 29, 2024
1 parent e153a6a commit 9a74756
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 86 deletions.
9 changes: 4 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ TODOx is a todo list app built with Go, HTMX, Tailwind CSS and SQLite to store t
The stack aims to be as simple as possible. It consists of:

- Go is the main language.
- Gostar to build the html.
- Gomponents to build the HTML.
- HTMX for the interactions with the user.
- Tailwind CSS with the Standalone CLI for styling.
- Tailwind CSS for styling (CDN).
- SQLite as the database.

## Architecture

The application is mostly written in Go and HTML. HTMX facilitates a lot of the interaction with the user on the frontend and the backend endpoints process requests and return HTML that will be then rendered by HTMX.

The Tailwind CSS Standalone CLI takes care of the styling by processing html files and adding resulting CSS to `internal/app/public/styles.css`. Any CSS in the public folder is served by the Go server. The storage of the application is SQLite.
The application is mostly written in Go and HTML. HTMX facilitates a lot of the interaction with the user on the frontend and the backend endpoints process requests and return HTML served by Go into the client side.
The storage of the application is a shiny SQLite database.

## Running in development

Expand Down
8 changes: 2 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ module todox
go 1.22.4

require (
github.com/delaneyj/gostar v0.7.2
github.com/gofrs/uuid/v5 v5.2.0
github.com/jmoiron/sqlx v1.3.5
github.com/leapkit/leapkit/core v0.0.36
github.com/mattn/go-sqlite3 v1.14.22
maragu.dev/gomponents v1.0.0
maragu.dev/gomponents-htmx v0.6.1
)

require (
github.com/go-playground/form/v4 v4.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.3.0 // indirect
github.com/igrmk/treemap/v2 v2.0.1 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
)
16 changes: 4 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/delaneyj/gostar v0.7.2 h1:hpmxebJR4XskCKYbu+inTJ/vPixBLFXOJSggTZ5ZJeA=
github.com/delaneyj/gostar v0.7.2/go.mod h1:mlxRWAVbntRR2VWlpXAzt7y9HY+bQtEm/lsyFnGLx/w=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
Expand All @@ -10,8 +8,6 @@ github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA
github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobuffalo/plush/v5 v5.0.1 h1:6enlV/9N5ma5+ql4ikKq3b+wjwOIe2JST4R0NVIXJaA=
github.com/gobuffalo/plush/v5 v5.0.1/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
Expand All @@ -20,8 +16,6 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/igrmk/treemap/v2 v2.0.1 h1:Jhy4z3yhATvYZMWCmxsnHO5NnNZBdueSzvxh6353l+0=
github.com/igrmk/treemap/v2 v2.0.1/go.mod h1:PkTPvx+8OHS8/41jnnyVY+oVsfkaOUZGcr+sfonosd4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/leapkit/leapkit/core v0.0.36 h1:z+ImNOB/AIS8ous43/qeACzZhM8x1plquyiDyW43fqQ=
Expand All @@ -31,9 +25,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
maragu.dev/gomponents v1.0.0 h1:eeLScjq4PqP1l+r5z/GC+xXZhLHXa6RWUWGW7gSfLh4=
maragu.dev/gomponents v1.0.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng=
maragu.dev/gomponents-htmx v0.6.1/go.mod h1:51nXX+dTGff3usM7AJvbeOcQjzjpSycod+60CYeEP/M=
34 changes: 27 additions & 7 deletions internal/todos/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"net/http"

"github.com/gofrs/uuid/v5"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"

. "github.com/delaneyj/gostar/elements"
hx "maragu.dev/gomponents-htmx"
)

func Edit(w http.ResponseWriter, r *http.Request) {
Expand All @@ -19,11 +21,29 @@ func Edit(w http.ResponseWriter, r *http.Request) {
return
}

el := LI().CLASS("gap-3 bg-white items-center rounded gap-2 p-3").Children(
FORM().CLASS("flex flex-row gap-3 mb-0").Children(
INPUT().VALUE(todo.Content).TYPE("text").NAME("content").CLASS("p-2 border rounded flex-grow"),
BUTTON().CLASS("p-2 px-3 bg-green-500 text-white rounded").Text("Save"),
).Attr("hx-put", "/todos/"+todo.ID.String()).Attr("hx-swap", "outerHTML").Attr("hx-target", "closest li"),
).Attr("hx-get", "/todos/"+todo.ID.String()+"/show").Attr("hx-swap", "outerHTML").Attr("hx-trigger", "keyup[event.keyCode==27] from:window")
el := Li(
Class("gap-3 bg-white items-center rounded gap-2 p-3"),

hx.Get("/todos/"+todo.ID.String()+"/show"),
hx.Swap("outerHTML"),
hx.Trigger("keyup[event.keyCode==27] from:window"),

Form(
Class("flex flex-row gap-3 mb-0"),

hx.Put("/todos/"+todo.ID.String()),
hx.Swap("outerHTML"),
hx.Target("closest li"),

Input(
Value(todo.Content), Type("text"), Name("content"), Class("p-2 border rounded flex-grow"),
),
Button(
Class("p-2 px-3 bg-green-500 text-white rounded"),
Text("Save"),
),
),
)

el.Render(w)
}
42 changes: 30 additions & 12 deletions internal/todos/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package todos
import (
"net/http"

. "github.com/delaneyj/gostar/elements"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"

hx "maragu.dev/gomponents-htmx"
)

func Index(w http.ResponseWriter, r *http.Request) {
Expand All @@ -15,20 +18,35 @@ func Index(w http.ResponseWriter, r *http.Request) {
return
}

page(
Group(
INPUT().ID("search").TYPE("search").NAME("keyword").Attr("hx-get", "/todos/search").Attr("hx-target", "#todoList").Attr("hx-swap", "innerHTML").Attr("hx-trigger", "keyup delay:200ms").PLACEHOLDER("Type in to search").CLASS("p-2 px-3 mb-2 w-full rounded border"),
HR().CLASS("border mb-2"),
UL().ID("todoList").CLASS("flex flex-col gap-2 mb-2").Children(
p := page(
Div(
Input(
Class("p-2 px-3 mb-2 w-full rounded border"),
ID("search"), Type("search"), Name("keyword"),
hx.Trigger("keyup delay:200ms"), hx.Get("/todos/search"), hx.Target("#todoList"), hx.Swap("innerHTML"),
Placeholder("Type in to search"),
),

Hr(Class("border mb-2")),
Ul(
ID("todoList"),
Class("flex flex-col gap-2 mb-2"),
todoListHTML(list),
),
DIV().CLASS("p-3 border bg-white items-center rounded gap-2").Children(
FORM().Attr("hx-post", "/todos/").Attr("hx-target", "#todoList").CLASS("flex flex-row gap-2 mb-0").Children(
INPUT().TYPE("text").ID("content").NAME("Content").CLASS("p-2 border rounded flex-grow").PLACEHOLDER("TODO Content").AUTOFOCUS(),
BUTTON().CLASS("bg-blue-500 rounded p-2 px-4 text-white").Text("Create"),

Div(
Class("p-3 border bg-white items-center rounded gap-2"),
Attr("_", "on htmx:afterRequest if detail.successful tell [#content,#search] set you.value to ''"),
Form(
Class("flex flex-row gap-2 mb-0 items-center"),
hx.Post("/todos/"), hx.Target("#todoList"),
Input(Type("text"), ID("content"), Name("Content"), Class("p-2 border rounded flex-grow"), Placeholder("TODO Content"), Attr("autofocus")),
Button(Class("bg-blue-500 rounded p-2 px-4 text-white"), Text("Create")),
),
).Attr("_", "on htmx:afterRequest if detail.successful tell [#content,#search] set you.value to ''"),
),
),
).Render(w)
)

p.Render(w)

}
35 changes: 22 additions & 13 deletions internal/todos/page.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
package todos

import . "github.com/delaneyj/gostar/elements"
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)

func page(yield ElementRenderer) ElementRenderer {
return HTML().Children(
HEAD().Children(
META().NAME("viewport").CONTENT("width=device-width, initial-scale=1"),
META().CHARSET("utf-8"),
TITLE().Text("Todo"),
SCRIPT().SRC("https://unpkg.com/[email protected]/dist/htmx.min.js"),
SCRIPT().SRC("https://unpkg.com/[email protected]"),
func page(yield Node) Node {
return HTML(
Head(
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
Meta(Charset("utf-8")),

LINK().REL("stylesheet").HREF("/public/application.css"),
TitleEl(Text("TodoX")),
Script(Src("https://unpkg.com/[email protected]/dist/htmx.min.js")),
Script(Src("https://unpkg.com/[email protected]")),
Script(Src("https://cdn.tailwindcss.com?plugins=forms,typography,line-clamp")),
),
BODY().CLASS("h-full bg-gray-100 pb-10 pt-10").Children(
DIV().CLASS("max-w-[1500px] mx-auto px-5").Children(
H1().CLASS("text-2xl mb-2 font-bold").Text("Todo List"),

Body(
Class("h-full bg-gray-100 pb-10 pt-10"),
Div(
Class("max-w-[1500px] mx-auto px-5"),
H1(
Class("text-2xl mb-2 font-bold"),
Text("TodoX List"),
),

// here goes the thing you want to render.
yield,
Expand Down
89 changes: 66 additions & 23 deletions internal/todos/todo.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,85 @@
package todos

import . "github.com/delaneyj/gostar/elements"

func todoHTML(t Instance) ElementRenderer {
return LI().CLASS("p-3 border flex flex-row items-center rounded gap-2").Children(
SPAN().CLASS("flex h-6 items-center").Children(
DIV(
INPUT().NAME("Completed").CHECKEDSet(t.Completed).TYPE("checkbox").ID("complete_"+t.ID.String()).Attr("hx-put", "/todos/"+t.ID.String()+"/complete").Attr("hx-include", "#not_complete_"+t.ID.String()).Attr("hx-target", "closest li").Attr("hx-swap", "outerHTML").Attr("aria-describedby", "comments-description").CLASS("h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"),
INPUT().NAME("Completed").TYPE("hidden").VALUE("false").ID("not_complete_"+t.ID.String()),
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"

hx "maragu.dev/gomponents-htmx"
)

func todoHTML(t Instance) Node {
return Li(
Class("bg-white p-3 border flex flex-row items-center rounded gap-2"),
If(t.Completed, Class("bg-gray-50")),
If(!t.Completed, Class("bg-white")),

Span(
Class("flex h-6 items-center"),
Div(
Input(
Name("Completed"), If(t.Completed, Checked()), Type("checkbox"), ID("complete_"+t.ID.String()),
hx.Put("/todos/"+t.ID.String()+"/complete"),
hx.Include("#not_complete_"+t.ID.String()),
hx.Target("closest li"),
hx.Swap("outerHTML"),

Class("h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"),
),
Input(
Name("Completed"), Type("hidden"), Value("false"), ID("not_complete_"+t.ID.String()),
),
),
),

SPAN().CLASS("flex-grow items-center").IfChildren(
t.Completed,
SPAN().CLASS("line-through").Text(t.Content),
).IfChildren(
!t.Completed,
SPAN().Attr("hx-get", "/todos/"+t.ID.String()+"/edit").Attr("hx-target", "closest li").Attr("hx-swap", "outerHTML").CLASS("cursor-pointer underline flex flex-row gap-2 items-center").Attr("_", "on htmx:afterRequest if detail.successful tell #content set you.value to ''").Children(
SPAN().Text(t.Content),
SVG_SVG().Attr("xmlns", "http://www.w3.org/2000/svg").Attr("fill", "none").Attr("viewBox", "0 0 24 24").Attr("stroke-width", "1.5").Attr("stroke", "currentColor").CLASS("w-4 h-4").Children(
SVG_PATH().Attr("stroke-linecap", "round").Attr("stroke-linejoin", "round").Attr("d", "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"),
Span(
Class("flex-grow items-center"),
If(t.Completed, Span(Class("line-through"), Text(t.Content))),
If(!t.Completed,
Span(
Class("cursor-pointer underline flex flex-row gap-2 items-center"),

Attr("_", "on htmx:afterRequest if detail.successful tell #content set you.value to ''"),
hx.Get("/todos/"+t.ID.String()+"/edit"),
hx.Target("closest li"),
hx.Swap("outerHTML"),

Span(Text(t.Content)),
Raw(
`<svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 -960 960 960" width="18px" fill="#5f6368">
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/>
</svg>`,
),
),
),
),

SPAN().CLASS("bg-red-500 text-white py-2 px-3 rounded cursor-pointer").Text("Delete").Attr("hx-delete", "/todos/"+t.ID.String()).Attr("hx-target", "closest li").Attr("hx-swap", "outerHTML").Attr("hx-confirm", "Are you sure you wish to delete this TODO?").Attr("_", "on htmx:afterRequest if detail.successful send keyup to #search"),
).IfCLASS(t.Completed, "bg-gray-50").IfCLASS(!t.Completed, "bg-white")
Span(
Class("bg-red-500 text-white py-2 px-3 rounded cursor-pointer"),
hx.Confirm("Are you sure you wish to delete this TODO?"),
hx.Delete("/todos/"+t.ID.String()),
hx.Swap("outerHTML"),
hx.Target("closest li"),
Text("Delete"),

Attr("_", "on htmx:afterRequest if detail.successful send keyup to #search"),
),
)
}

func todoListHTML(todos []Instance) ElementRenderer {
func todoListHTML(todos []Instance) Group {
if len(todos) == 0 {
return LI().CLASS("p-10 bg-white rounded border text-center").Text("No todos found.")
return []Node{
Li(
Class("p-10 bg-white rounded border text-center"),
Text("No todos found."),
),
}
}

els := []ElementRenderer{}
els := []Node{}
for _, todo := range todos {
els = append(els, todoHTML(todo))
}

return Group(els...)
return els
}
8 changes: 0 additions & 8 deletions tailwind.config.js

This file was deleted.

0 comments on commit 9a74756

Please sign in to comment.