Setting up Go templ with Tailwind, HTMX and Docker
My endless quest to find a decent frontend framework
DECLAIMER: This section is a rant, you won’t miss anything if you skip it.
I have been a web developer since I created this project, which was my first web project and second Go project, I liked Go since I created this console Tetris thingy, but before Go I had an another quest looking for a cromulent daily (multi-puprose) usable language, before Go I was using C++ for a while (until I couldn’t take it anymore), and had a run with Kotlin, Python, Java, and C. even though I jumped ship a lot between these languages (I was a junior in college, so I’m allowed to do this), they didn’t drive me crazy or anything, they just didn’t really fit my day to day usage, for example, Java and Kotlin required me to use a fancy IDE which my computer wasn’t really on board with it.
So finally I found Go, and it really did it for me, writing reliable automation scripts, readability, smooth OOP, portability, and most importantly speed where even with stupid unoptimized code, Go is still fast!
And when I started learning web, Go was and still is my first choice to quickly write a fast and “reliable” backend, the problem was with frontend, I jumped a lot of ships with frontend, because back when I was learning web, I was a huge fan of OOP (still kinda is, but a bit lesser) and most frontend frameworks are FRAMEWORKS, where they have their structure forced on us, so the initial frontend of the GDSC Logo Generator was in pure HTML/CSS/JavaScript, it did it’s job, but I didn’t much like how the code looked, so as I was learning Vue, the first rewrite was in Vue (and it was my last Vue project), I kinda enjoyed Vue for a while, but I hated that I have to write JavaScript and handle types manually, especially objects coming from the backend, and Vue3 (it supported TypeScript) was an early beta, so it didn’t really work for me.
So that’s when I tried React for the first time, React was a banger for me, since it wasn’t trying to be a framework and force an architecture on me, and it had TypeScript out of the box, the thing was, React was blazingly slow, that I jumped ship faster than Vue, I didn’t even rewrite the logo generator with it, I also tried Next which is the full stack thing for React, but it was also in its early stages, and it was buggy AF, I did write my website with it though, but that was it, because I spent time dealing with Next’s issues, more than focusing on the actual product.
And that’s where I met SvelteKit, it really did it for me, I rewrote my website, the logo generator, did some more side projects apollo-music was the only one that saw the light of day, and did a lot of freelance projects with it. So what made me jump ship you might ask if you refer to my previous post will have the answer, TLDR; I messed up some server code with TypeScript that I came to the conclusion that a rewrite is better than fixing TypeScript server fuck ups, after this I was looking for a non JS/TS frontend framework, I found Yew and Leptos which are Rust frontend frameworks, and I went with Yew, both are great, and if I want to do a serious client intractive application I’d go with either of them.
Honoroble mentions:
- htmx the frontend library of peace, which is great for some client/server side mix ups, I think it can be used instead of Yew and Leptos, but I didn’t tinker much with it, and it involves some JavaScript, so IDK, well, we’re gonna use it today, so, YAY.
- Go Templates I like the server side blazing fast render time of the Go templates, their problem is that they just work (and type safity), that’s why they created templ, but either way they’re great, especially with htmx, and they do their job and their job only, so you won’t drown in the frontend voodoo magic.
- Nuxt the thing I use at work, it’s to Vue what Next is to React, I hate Nuxt, but I gotta admit that half my salary comes from it, so I like Nuxt 👍
Finally I found templ, and I was really excited, that I can finally write frontends with my favorite language, and with a lot of stuff available out of the box (which are the reason I went with it over Go templates), you get awesome editor support, a cool cli, components, interoperability with other stuff like Go templates and React, live reload (kinda part of the cli but it’s there), and YOU GET TO WRITE FRONTEND WITH GO, can you imagine this very small binary sizes, blazingly fast build times, because I was once building a Yew application on my very hardworking server and cargo literally halted the server for 30 minutes, that I had to restart it :( (I had 324 days of uptime). So yeah templ looks very promesing to me, that I’m building yet another home page called Chateau Web that has some stuff that are usually needed in a home page, and I’m kinda pushing my designer skills with it, you can find it here, if you find nothing, it probably means that I didn’t finish it yet :)
Let’s dig in…
Installing the templ cli
The CLI will be used to generate Go code from .templ
files, and can also be used to run a hot-reloadable server, so yeah, we kinda need it installed.
And I’m just following the official docs, no personal voodoo magic here.
With Go 1.20 or greater installed, run:
go install github.com/a-h/templ/cmd/templ@latest
Editor setup
Neovim (lsp-zero)
I use this setup function with my Neovim setup, in which it takes care of the whole templ stuff, and if I decided not to use it, I simply don’t call it.
This function has some parts from the official docs, and the rest are some scattered parts from multiple sources.
-- ~/.config/nvim/after/plugin/templ.lua
-- IDK where is the neovim configuration on Mac or Windows,
-- so you need to do some research :)
local function setup_templ()
local lspconfig = require 'lspconfig'
local configs = require 'lspconfig.configs'
-- start the templ language server for go projects with .templ files
configs.templ = {
default_config = {
cmd = { "templ", "lsp", "-http=localhost:7474", "-log=/tmp/templ.log" },
filetypes = { "templ" },
root_dir = lspconfig.util.root_pattern("go.mod", ".git"),
settings = {},
},
}
lspconfig.templ.setup{}
-- register .templ as a filetype
vim.filetype.add({ extension = { templ = "templ" } })
lspconfig.html.setup({
on_attach = lsp.on_attach,
capabilities = lsp.capabilities,
filetypes = { "html", "templ" },
})
-- htmx, the frontend library of peace
lspconfig.htmx.setup({
on_attach = lsp.on_attach,
capabilities = lsp.capabilities,
filetypes = { "html", "templ" },
})
-- needed tailwindcss classes auto complete
lspconfig.tailwindcss.setup({
on_attach = lsp.on_attach,
capabilities = lsp.capabilities,
filetypes = { "templ", "astro", "javascript", "typescript", "react" },
init_options = { userLanguages = { templ = "html" } },
})
-- needed for auto tag insertion
lspconfig.emmet_ls.setup({
on_attach = lsp.on_attach,
capabilities = lsp.capabilities,
filetypes = { "templ", "astro", "javascript", "typescript", "react" },
init_options = { userLanguages = { templ = "html" } },
})
-- format thingy
vim.api.nvim_create_autocmd({ "BufWritePost" }, { -- IDK the docs said to do the format before saving the file, but it only makes the formatter freak out.
pattern = { "*.templ" },
callback = function()
local file_name = vim.api.nvim_buf_get_name(0) -- Get file name of file in current buffer
vim.cmd(":silent !templ fmt " .. file_name)
local bufnr = vim.api.nvim_get_current_buf()
if vim.api.nvim_get_current_buf() == bufnr then
vim.cmd('e!')
end
end
})
end
setup_templ()
Now just restart Neovim, and it should load the templ language server, with formatting and everything.
Other Editors
You can check templ’s official docs for other editros :)
Project structure
We’re building a spending logs application, and we’ll be using a structure similar to the one in the official docs which is an MVC-like structure, with the packages and files as described below:
components/
- templ components.db/
- Database access code used to access the spending logs.handlers/
- HTTP handlers.static/
- Files that are available to the public.services/
- Services used by the handlers..gitignore
- Some stuff are not worthy of being committed.Dockerfile
- Container configuration to run the application with the glorious Docker.Makefile
- A runner and builder script to run the templ thing alongside tailwindcss, and it has the build commands.main.go
- The entrypoint to our application.
The final project is available here.
Hello templ
Boilerplate setup
Let’s start by initializing a Go module
go mod init spendings
Then adding templ to the project
go get github.com/a-h/templ
Now create the packages as described above
mkdir components db handlers static services tailwindcss
Now for the .gitignore
, we’ll be ignoring generated Go templ files, the tailwind output file, tailwind’s node modules, and the go binary.
# .gitignore
*templ.go
static/css/tailwind.css
node_modules/
spendings
Now create the Makefile, more info here
Basically what this Makefile does is that it has 3 targets (like npm scripts) but the GNU people did it before it was cool! So we have those targets:
build
- Compiles and minifies the tailwind stylesheet to be used with the thing.dev
- Runs the tailwind and the templ watcher, where we get a true live reload feeling (you still have to refresh the page in the browser tho)clean
- It’s a given having aclean
target in a Makefile, this only deletes the output Go binary.
The .PHONY
directive sets the default make target, so when you just run make
it actually runs make build
# Makefile
.PHONY: build
BINARY_NAME=spendings
# build builds the tailwind css sheet, and compiles the binary into a usable thing.
build:
go mod tidy && \
templ generate && \
go generate && \
go build -ldflags="-w -s" -o ${BINARY_NAME}
# dev runs the development server where it builds the tailwind css sheet,
# and compiles the project whenever a file is changed.
dev:
templ generate --watch --cmd="go generate" &\
templ generate --watch --cmd="go run ."
clean:
go clean
To use the make file, in the root of the project run
make # or
make build # to build the thing
make dev # to start the development server
And that’s make for you, totally unrelated but it can be handy when dealing with scripts. Sadly I’m not really sure if it runs on Windows, so you’re gonna have to find out yourself.
Your First templ component
Under components create a file named greet.templ
with the following content
// components/greet.templ
package components
import "fmt"
templ Greet(name string, age int) {
<p>Hi I'm { name }, and I'm { fmt.Sprint(age) } years old!</p>
}
To generate the Regular Go code from the .templ
file, run
templ generate
Setup a simple http server to host this glorious templ component.
// main.go
package main
import (
"log"
"net/http"
"spendings/components"
"github.com/a-h/templ"
)
func main() {
greet := components.Greet("Lizzy The Cat", 2)
handler := templ.Handler(greet)
log.Fatalln(http.ListenAndServe(":8080", handler))
}
Tailwind CSS?
Before we do anything related to Tailwind CSS, we need to setup a layout component, so that we look fancy like the other frontend devs.
So the reason why we need the layout component, is to hold the links imports such as HTMX and Tailwind’s stylesheet and so they’re not duplicated in each page.
Under components
Create the files layout.templ
and index.templ
// components/layout.templ
package components
templ Layout(children ...templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>The Spending Log Thingy</title>
<!--
This line is literally why we created the layout component.
Actually having a standard html thing is why, but yeah it's what it's!
-->
<link href="/static/css/tailwind.css" rel="stylesheet"/>
</head>
<body>
for _, child := range children {
@child
}
</body>
</html>
}
So far this will be our base layout for any page we create.
// components/index.templ
package components
templ main() {
<main>something in main</main>
}
templ Index() {
@Layout(main())
}
This is the home page for the website, as you can see we passed a component main
to the Layout
component, we can pass more since it’s a variadic function, but for now just pass the main component.
Set up a router for the home page, the static directory in main.go
, and add the go generate
comment so the go tool can understand this directive and build tailwind’s css when go generate
is ran.
package main
import (
"embed"
"log"
"net/http"
"spendings/components"
"github.com/a-h/templ"
)
//go:embed static/*
var static embed.FS
//go:generate npx tailwindcss build -i static/css/style.css -o static/css/tailwind.css -m
func main() {
homePage := components.Index()
pagesHandler := http.NewServeMux()
pagesHandler.Handle("/", templ.Handler(homePage))
pagesHandler.Handle("/static/", http.FileServer(http.FS(static)))
log.Fatalln(http.ListenAndServe(":8080", pagesHandler))
}
Back to tailwind, run these commands to install the tailwind stuff.
npm install -D tailwindcss
npx tailwindcss init
Then configure the content path to fit templ’s needs
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./components/*.templ"],
theme: {
extend: {},
},
plugins: [],
};
Pre-Finally create an input stylesheet, so that you can add fonts and other classes to it, for now it’ll just contain the tailwind directives.
/* static/css/style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Finally add some tailwind styling to a component, to see it in the work.
// components/index.templ
package components
// also added this freakish not JavaScript JavaScript function as a demonstration to functions and events
script doTheThing() {
window.alert("yoho")
}
templ main() {
<main>
something in main
<button
onClick={ doTheThing() }
class="bg-pink-600 rounded-md"
>
Click me
</button>
</main>
}
templ Index() {
@Layout(main())
}
Now go back to the root of the project, and you should be able to run the dev server using make.
make dev
If you don’t have or don’t want to use make for some reason, you can run the commands sepratly, in two different terminals (in the root of the project)
templ generate --watch --cmd="go generate"
templ generate --watch --cmd="go run ."
And that’s exactly why I’m using a makefile, now let’s have some peace with htmx.
HTMX / the frontend library of peace (same thing)
Download the latest version of htmx.min.js
from here, as of the time I wrote this post, the latest version is 1.9.10
so the version might differ.
Save the file into static/js
mkdir static/js
cd static/js
wget https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js
And add this link import thingy to the <head>
section in the layout.
<!-- components/layout.templ -->
<script src="/static/js/htmx.min.js"></script>
Well, that’s it, more htmx stuff can be found in the waltz section
Docker (it doesn’t only work on your machine)
I kinda explained docker in more details in a previous post of mine, so go check it out if you have no idea what docker is.
# Dockerfile
FROM golang:1.22-alpine as build
WORKDIR /app
COPY . .
RUN go install github.com/a-h/templ/cmd/templ@latest &&\
apk add make npm nodejs &&\
make
FROM alpine:latest as run
WORKDIR /app
COPY --from=build /app/spendings ./run
COPY --from=build /app/db.json ./db.json
EXPOSE 8080
CMD ["./run"]
But just a note, as you can see I’m not copying any of the static files into the container, that’s because I’ve embedded them into the Go binary
//go:embed static/*
var static embed.FS
This copies the pointed at directory into the Go binary, so no need to copy them to the container, and this is the best way to serve SMALL static website assets in Go, they explained it briefly in here.
Let’s Waltz this out
At this point your project is ready, and you can start hacking with it, but you can continue reading to create a full working project with the whole mix to see how things go.
Waltz, TLDR; it’s a dance, so let’s dance with the frontend.
The project is really simple, it’s just there to demonstrate some of the core concepts of the gang {templ, htmx, TailwindCSS, and Go}
And it’ll look something like this at the end
We’ll start from the bottom up to the top, i.e. starting from the database ending with templ views.
Database
Types
For starters let’s define some types, starting with the spent item’s model, which will look like this.
// db/db.go
type Spending struct {
Id string `json:"id"`
Reason string `json:"reason"`
Price int64 `json:"price"`
SpentAt time.Time `json:"spent_at"`
}
This contains details about the item that was bought or sold, negative or positive change in the balance.
The SpendingStore
is an interface the its implementations will represent a data store with minimal CRUD operations for spendings.
// db/db.go
type SpendingsStore interface {
Insert(Spending) error
GetAll() ([]Spending, error)
Update(id string, values Spending) error
Delete(id string) error
}
Same but for the balance.
// db/db.go
type BalanceStore interface {
GetBalance() int64
SetBalance(int64) error
}
JSON Database
We’ll be using a JSON database, but since we have 2 interfaces representing the stores, the underlying database implementation doesn’t matter.
Create a file called json_db.go
under the db
package.
Add the schema of the stored database.
type storeSchema struct {
Spendings []Spending `json:"spending"`
Balance int64 `json:"balance"`
}
Now create a constant called dbFilePath, you can use an environment variable, but for now just hard code it, and we’ll create a struct called storeManager
which will handle writing and reading data into and from the database file.
// db/json_db.go
const dbFilePath = "./db.json"
// will be used with the json database implementations
var jsonMgr = &storeManager{}
// create the db file if not exists
func init() {
_, err := os.Stat(dbFilePath)
if os.IsNotExist(err) {
_, err = os.Create(dbFilePath)
if err != nil {
panic(err)
}
}
}
type storeManager struct {
// mu is to make the manager concurrently safe
mu sync.RWMutex
}
func (s *storeManager) get() (storeSchema, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var store storeSchema
f, err := os.Open(dbFilePath)
if err != nil {
return storeSchema{}, err
}
err = json.NewDecoder(f).Decode(&store)
if errors.Is(err, io.EOF) {
return storeSchema{}, nil
}
if err != nil {
return storeSchema{}, err
}
return store, nil
}
func (s *storeManager) set(store storeSchema) error {
s.mu.Lock()
defer s.mu.Unlock()
formattedJson, err := json.MarshalIndent(store, "", " ")
if err != nil {
return err
}
f, err := os.OpenFile(dbFilePath, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
_, err = f.Write(formattedJson)
if err != nil {
return err
}
return nil
}
After this implement SpendingsStore
and BalanceStore
to CRUD the json file that was set earlier.
This is a placeholder implementation of the database, but just to show sync.RWMutex
inside of the store, so that concurrent operations can be done, since http requests are handled cocurrently and we don’t want any sort of conflict.
// db/json_db.go
type SpendingsStoreJson struct {
mu sync.RWMutex
}
func NewSpendingsStoreJson() SpendingsStore {
return &SpendingsStoreJson{}
}
func (s *SpendingsStoreJson) Insert(_ Spending) error {
s.mu.Lock()
defer s.mu.Unlock()
panic("not implemented") // TODO: Implement
}
func (s *SpendingsStoreJson) GetAll() ([]Spending, error) {
s.mu.RLock()
defer s.mu.RUnlock()
panic("not implemented") // TODO: Implement
}
func (s *SpendingsStoreJson) Update(id string, values Spending) error {
s.mu.Lock()
defer s.mu.Unlock()
panic("not implemented") // TODO: Implement
}
func (s *SpendingsStoreJson) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
panic("not implemented") // TODO: Implement
}
type BalanceStoreJson struct {
mu sync.RWMutex
}
func NewBalanceStoreJson() BalanceStore {
return &BalanceStoreJson{}
}
func (b *BalanceStoreJson) GetBalance() int64 {
b.mu.RLock()
defer b.mu.RUnlock()
panic("not implemented") // TODO: Implement
}
func (b *BalanceStoreJson) SetBalance(_ int64) error {
b.mu.Lock()
defer b.mu.Unlock()
panic("not implemented") // TODO: Implement
}
As you can see each of the operations locks the database for any data modification until the operation is done defer mu.Unlock()
, and this will keep the file safe from any sort of data racing.
Now for the actual implementation, I’ll slap the code here, with comments explaining some of the operation.
type SpendingsStoreJson struct {
mu sync.RWMutex
}
func NewSpendingsStoreJson() SpendingsStore {
return &SpendingsStoreJson{}
}
func (s *SpendingsStoreJson) Insert(spending Spending) error {
s.mu.Lock()
defer s.mu.Unlock()
store, err := jsonMgr.get()
if err != nil {
return err
}
spending.Id = generateId()
store.Spendings = append(store.Spendings, spending)
// this is bad practice updating the balance from the spendings store wrapper, but again this is just a proof of concept db
store.Balance -= spending.Price
err = jsonMgr.set(store)
if err != nil {
return err
}
return nil
}
// generateId generates an id by hashing the current timestamp
func generateId() string {
sha256 := sha256.New()
sha256.Write([]byte(time.Now().String()))
return hex.EncodeToString(sha256.Sum(nil))
}
func (s *SpendingsStoreJson) GetAll() ([]Spending, error) {
s.mu.RLock()
defer s.mu.RUnlock()
store, err := jsonMgr.get()
if err != nil {
return nil, err
}
return store.Spendings, nil
}
func (s *SpendingsStoreJson) Update(id string, values Spending) error {
s.mu.Lock()
defer s.mu.Unlock()
store, err := jsonMgr.get()
if err != nil {
return err
}
// find element by id, using the fancy `slices.IndexFunc`
idx := slices.IndexFunc(store.Spendings, func(s Spending) bool {
return s.Id == id
})
if idx == -1 {
return errors.New("item was not found")
}
// update balance before the update
// this is bad practice updating the balance from the spendings store wrapper, but again this is just a proof of concept db
store.Balance += store.Spendings[idx].Price
store.Balance -= values.Price
// update the item's value
store.Spendings[idx] = values
err = jsonMgr.set(store)
if err != nil {
return err
}
return nil
}
func (s *SpendingsStoreJson) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
store, err := jsonMgr.get()
if err != nil {
return err
}
// find element by id, using the fancy `slices.IndexFunc`
idx := slices.IndexFunc(store.Spendings, func(s Spending) bool {
return s.Id == id
})
if idx == -1 {
return errors.New("item was not found")
}
// update balance before the deletion
store.Balance += store.Spendings[idx].Price
// delete item and remove its entry from the slice
// this is bad practice updating the balance from the spendings store wrapper, but again this is just a proof of concept db
store.Spendings = append(store.Spendings[:idx], store.Spendings[idx+1:]...)
err = jsonMgr.set(store)
if err != nil {
return err
}
return nil
}
type BalanceStoreJson struct {
mu sync.RWMutex
}
func NewBalanceStoreJson() BalanceStore {
return &BalanceStoreJson{}
}
func (b *BalanceStoreJson) GetBalance() int64 {
b.mu.RLock()
defer b.mu.RUnlock()
store, err := jsonMgr.get()
if err != nil {
return 0
}
return store.Balance
}
func (b *BalanceStoreJson) SetBalance(newBalance int64) error {
b.mu.Lock()
defer b.mu.Unlock()
store, err := jsonMgr.get()
if err != nil {
return err
}
store.Balance = newBalance
err = jsonMgr.set(store)
if err != nil {
return err
}
return nil
}
Services
The services are the section of any application that performs the business logic and stuff, since the handlers and views are only intermediates to represent data to the user, and modify the state of the application using a fancy UI, such as interactive views (html) or reusable APIs (REST). And since this is a tiny CRUD application where the data layer actually the logic layer, so the services will only redirect data from the views and handlers to the database, the reason why it’s done this way, so that the views won’t have a direct contact with the data layer, and changing the logic or the data layer can happen away from the views.
We’ll only be implementing two services spending and balance, so let’s get started.
And again I’ll just slap the code here, with some comments, so you could just copy and paste it.
Usually some data validation is done through a service, so that the data reach the data store as clean as possible, and errors returned from the data layer are as minimal as possible, since those transtactions usually take time, especially if the database is on another server.
// services/spendings.go
package services
import "spendings/db"
type SpendingsService struct {
store db.SpendingsStore
}
func NewSpendingService(store db.SpendingsStore) *SpendingsService {
return &SpendingsService{store}
}
func (s *SpendingsService) AddItem(spending db.Spending) error {
spending.SpentAt = time.Now()
return s.store.Insert(spending)
}
func (s *SpendingsService) ListItems() ([]db.Spending, error) {
return s.store.GetAll()
}
func (s *SpendingsService) UpdateItem(id string, newValue db.Spending) error {
return s.store.Update(id, newValue)
}
func (s *SpendingsService) DeleteItem(id string) error {
return s.store.Delete(id)
}
The balance service is kinda small, since the only operation that we want to expose from the database is GetBalance
, because the balance update stuff are in the spendings store.
// services/balance.go
package services
import "spendings/db"
type BalanceService struct {
store db.BalanceStore
}
func NewBalanceService(store db.BalanceStore) *BalanceService {
return &BalanceService{store}
}
func (b *BalanceService) GetBalance() int64 {
return b.store.GetBalance()
}
Handlers & Views
This is were we part ways, since it’s the last part of this post, here we’ll create the usable views of the applications, and the needed endpoints to update the spending logs.
Stuff in here will be split faily between templ and htmx, where templ will handle the get operations, since it’s the view part of our application, and htmx will handle the add, update and delete operations, since those are not handled by the http method GET
and require the usage of other methods, in which it doesn’t make any sense doing them from the template.
Implementing templ components
Revisiting the index page we implemented earlier, in which it’ll be the page that displays the balance and the spending logs.
We’ll add the balance and spendings to the component’s props, so that, it can be passed down to the components that will display it.
// components/index.templ
package components
import "spendings/db"
templ Index(balance int64, spendings []db.Spending) {
<main class="w-full h-screen bg-pink-100">
@Layout(Balance(balance))
</main>
}
Now create two files under components
called balance.templ
and spendings.templ
, which will hold the main sections of the app.
We’ll start by implementing balance.templ
since it’s just a small component.
// components/index.templ
package components
import "fmt"
templ Balance(b int64) {
<section class="w-full pt-5">
<div class="m-auto w-fit py-3 px-12 border border-red-300 rounded-lg">
<span class="text-xl">Current Balance: <b>{ fmt.Sprint(b) }</b></span>
</div>
</section>
}
Now for spendings.templ
// components/spendings.templ
package components
import "spendings/db"
import "fmt"
templ Spendings(spendings []db.Spending) {
<section class="w-full pt-5">
<div class="m-auto w-fit flex flex-col gap-2">
for _, s := range spendings {
<div
class={ "rounded-md p-2 min-w-[400px] ",
// green for spent, red for gained
templ.KV("bg-green-400", s.Price < 0),
templ.KV("bg-red-400", s.Price > 0) }
>
<div class="flex justify-between">
<div>
<span class="font-bold text-lg">{ s.Reason }</span>
: <span>${ fmt.Sprint(s.Price) }</span>
</div>
<span>{ s.SpentAt.Format("01-Feb-2006") }</span>
</div>
<div class="float-right">
<button class="font-bold uppercase bg-purple-300 hover:bg-white py-1 px-4 rounded-xl border-purple-300">Delete</button>
<button class="font-bold uppercase bg-blue-300 hover:bg-white py-1 px-4 rounded-xl border-purple-300">Update</button>
</div>
</div>
}
</div>
</section>
}
Now we need to update index.templ
, to add the Spendings component.
// components/index.templ
package components
import "spendings/db"
templ Index(balance int64, spendings []db.Spending) {
@Layout(main(balance, spendings))
}
templ main(balance int64, spendings []db.Spending) {
<main class="w-full h-screen bg-pink-100">
@Balance(balance)
@Spendings(spendings)
</main>
}
Then do some updates to main.go
to fetch the data from the database.
package main
import (
"context"
"embed"
"log"
"net/http"
"spendings/components"
"spendings/db"
"spendings/services"
)
//go:embed static/*
var static embed.FS
//go:generate npx tailwindcss build -i static/css/style.css -o static/css/tailwind.css -m
func main() {
ctx := context.Background()
balanceStore := db.NewBalanceStoreJson()
spendingsStore := db.NewSpendingsStoreJson()
balanceService := services.NewBalanceService(balanceStore)
spendingsService := services.NewSpendingService(spendingsStore)
pagesHandler := http.NewServeMux()
// it was needed to return the page from a handler function,
// so that fetching data from the database is done for each request.
pagesHandler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
spendings, err := spendingsService.ListItems()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
components.Index(balanceService.GetBalance(), spendings).Render(ctx, w)
})
pagesHandler.Handle("/static/", http.FileServer(http.FS(static)))
log.Println("starting server on port 8080")
log.Fatalln(http.ListenAndServe(":8080", pagesHandler))
}
Now that main.go
is utilizing the database, the file db.json
is generated, since it was in the init
of the db
package.
Now you need modify it now to set your balance, and don’t worry about the spendings, it’ll be handled by the json data stores we wrote earlier.
{
"spendings": [],
"balance": 1234
}
Implementing add, update and delete endpoints
For the handler, to complete the cycle, we’ll create a struct hodling the endpoints then handle them using http.HandleFunc
, and since Go has recently added specifieing the endpoint’s method in version 1.22, and this is Go’s official docs for the new mux thingy Routing Enhancements for Go 1.22, this will be an easy task.
Under handlers
create a file called spendings.go
to write the handlers’ logic in it, and since the balance is automatically updated from the spendings store (read above, not gonna explain myself again…), and it’s value fetched into the view directly, so there’s no need to implement a REST api for it.
// handlers/spendings.go
package handlers
import (
"encoding/json"
"net/http"
"spendings/db"
"spendings/services"
)
type SpendingsHandler struct {
service services.SpendingsService
}
func NewSpendingHandler(service services.SpendingsService) *SpendingsHandler {
return &SpendingsHandler{service}
}
func (s *SpendingsHandler) HandleAddSpendingItem(w http.ResponseWriter, r *http.Request) {
var spending db.Spending
err := json.NewDecoder(r.Body).Decode(&spending)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = s.service.AddItem(spending)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *SpendingsHandler) HandleRemoveSpendingItem(w http.ResponseWriter, r *http.Request) {
id, exists := r.URL.Query()["id"]
if !exists {
w.WriteHeader(http.StatusBadRequest)
return
}
err := s.service.DeleteItem(id[0])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *SpendingsHandler) HandleUpdateSpendingItem(w http.ResponseWriter, r *http.Request) {
id, exists := r.URL.Query()["id"]
if !exists {
w.WriteHeader(http.StatusBadRequest)
return
}
var newSpending db.Spending
err := json.NewDecoder(r.Body).Decode(&newSpending)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = s.service.UpdateItem(id[0], newSpending)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
Finally update main.go
to add the new handlers, and group the pages and rest handlers into separate http.ServeMux
.
// main.go
package main
import (
"context"
"embed"
"log"
"net/http"
"spendings/components"
"spendings/db"
"spendings/handlers"
"spendings/services"
)
//go:embed static/*
var static embed.FS
//go:generate npx tailwindcss build -i static/css/style.css -o static/css/tailwind.css -m
func main() {
ctx := context.Background()
balanceStore := db.NewBalanceStoreJson()
spendingsStore := db.NewSpendingsStoreJson()
balanceService := services.NewBalanceService(balanceStore)
spendingsService := services.NewSpendingService(spendingsStore)
pagesHandler := http.NewServeMux()
pagesHandler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
spendings, err := spendingsService.ListItems()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
components.Index(balanceService.GetBalance(), spendings).Render(ctx, w)
})
pagesHandler.Handle("/static/", http.FileServer(http.FS(static)))
spendingsHandler := handlers.NewSpendingHandler(*spendingsService)
restHandler := http.NewServeMux()
restHandler.HandleFunc("POST /spending", spendingsHandler.HandleAddSpendingItem)
restHandler.HandleFunc("PUT /spending", spendingsHandler.HandleUpdateSpendingItem)
restHandler.HandleFunc("DELETE /spending", spendingsHandler.HandleRemoveSpendingItem)
applicationHandler := http.NewServeMux()
applicationHandler.Handle("/", pagesHandler)
applicationHandler.Handle("/api/", http.StripPrefix("/api", restHandler))
log.Println("starting server on port 8080")
log.Fatalln(http.ListenAndServe(":8080", applicationHandler))
}
Making peace with htmx
Of course htmx will be the last topic, since it’s the most elegant thing in here, and it’s really fun to write.
But before we do anything, we need an htmx extension called json-enc
to send requests as json using hx-post
, now go into the static/js
directory, and download the thing.
cd static/js
wget https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js
Then update components/layout.templ
and add this import line, under the <head>
section.
<script src="/static/js/json-enc.js"></script>
And we need to make a little modification to json-enc.js
to handle numeric values properly, cuz otherwise it’ll just send strings instead of numbers.
Update the encodeParameters method in the object thingy, just add this freakish loop and you’re good to go.
htmx.defineExtension("json-enc", {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers["Content-Type"] = "application/json";
}
},
// modify here
encodeParameters: function (xhr, parameters, elt) {
xhr.overrideMimeType("text/json");
for (const key in parameters) {
const tryNum = parseFloat(parameters[key]);
// using == to check only the value against the string
if (parameters[key] == tryNum) {
parameters[key] = tryNum;
}
}
return JSON.stringify(parameters);
},
});
Now for each spending endpoint handler, we need to add a header called HX-Redirect
, where this redirects the page after the response reaches the browser, and we’re gonna make it redirect to /
so it refreshes the thing.
w.Header().Set("HX-Redirect", "/")
And this is the final version of spendings.templ
, where I added a form to add a new item, and hx-delete
tag on the delete button, I didn’t add the update thingy because I’m lazy.
// components/spendings.templ
package components
import "spendings/db"
import "fmt"
templ newSpending() {
<div
class="p-5 rounded-xl bg-blue-300"
>
<h2>Add new item</h2>
<form
hx-post="/api/spending"
hx-ext="json-enc"
hx-target="this"
hx-swap="none"
>
<input type="text" name="reason" placeholder="Reason" required/>
<input type="number" min="-2000" max="2000" name="price" placeholder="Price" required/>
<button type="submit" class="font-bold uppercase bg-purple-300 hover:bg-white py-1 px-4 rounded-xl border-purple-300">Add</button>
</form>
</div>
}
templ Spendings(spendings []db.Spending) {
<section class="w-full pt-5">
<div class="m-auto w-fit flex flex-col gap-2">
@newSpending()
for _, s := range spendings {
<div
class={ "rounded-md p-2 min-w-[400px] ",
templ.KV("bg-green-400", s.Price < 0),
templ.KV("bg-red-400", s.Price > 0) }
>
<div class="flex justify-between">
<div>
<span class="font-bold text-lg">{ s.Reason }</span>
: <span>${ fmt.Sprint(s.Price) }</span>
</div>
<span>{ s.SpentAt.Format("01-Feb-2006") }</span>
</div>
<div class="float-right">
<button
class="font-bold uppercase bg-purple-300 hover:bg-white py-1 px-4 rounded-xl border-purple-300"
hx-delete={ fmt.Sprintf("/api/spending?id=%s", s.Id) }
>
Delete
</button>
<button
class="font-bold uppercase bg-blue-300 hover:bg-white py-1 px-4 rounded-xl border-purple-300"
>
Update
</button>
</div>
</div>
}
</div>
</section>
}
And now, we’re done, hope you found this useful!
Quote of the day
“He who conquers others is strong; He who conquers himself is mighty.”
- Lao Tzu