In today’s web development landscape, JavaScript has long been the language of choice for creating dynamic and interactive web applications.
As a Go developer, what if you don’t want to use Javascript and still implement a responsive web application?
Imagine a sleek to-do list app that updates instantly as you check off tasks without a full-page reload. This is the power of Golang and htmx!
Combining Go and htmx allows us to create responsive and interactive web applications without writing a single line of JavaScript.
In this blog, we will explore how to use htmx and Golang to build web applications. (It can be used with other your favorite platforms, too.)
As a learning, we will implement basic create and delete operations for users.
htmx is a modern HTML extension that adds bidirectional communication between the browser and the server.
It allows us to create dynamic web pages without writing JavaScript, as it provides access to AJAX, server-sent events, etc in HTML directly.
hx-get="/my-endpoint"
).hx-target
and hx-swap
attributes. This can involve: — Replacing the entire element’s content.
— Inserting new content before or after the element.
— Appending content to the end of the element.
Let’s understand it in more depth with an example.
<button hx-get="/fetch-data" hx-target="#data-container">
Fetch Data
</button>
<div id="data-container"></div>
In the above code, when the button is clicked:
/fetch-data
.#data-container
element.Below are the required tools/frameworks to build this basic app.
main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Run(":8080")
fmt.Println("Server is running on port 8080")
}
It sets up a basic Go server, running at port 8080.
Rungo run main.go
to run the application.
users.html
<!DOCTYPE html>
<html>
<head>
<title>Go + htmx app </title>
<script src="https://unpkg.com/[email protected]" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw" crossorigin="anonymous"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="text-center flex flex-col w-full gap-6 mt-10">
<table id="user-list" class="w-1/2 mx-auto mt-4 border border-gray-300">
<thead>
<tr class="border border-gray-300">
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Email</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
{{ range .users }}
<tr class="border border-gray-300">
<td class="px-4 py-2">{{ .Name }}</td>
<td class="px-4 py-2">{{ .Email }}</td>
<td class="px-4 py-2">
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
</td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>
We have included,
- htmx using the script tag — https://unpkg.com/[email protected]
- Tailwind CSS with cdn link — https://cdn.tailwindcss.com
Now, we can use Tailwind CSS classes and render the templates with htmx.
As we see in users.html
, we need to pass users array to the template, so that it can render the users list.
For that let’s create a hardcoded static list of users and create a route to render users.html
.
main.go
package main
import (
"fmt"
"net/http"
"text/template"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
users := GetUsers()
tmpl := template.Must(template.ParseFiles("users.html"))
err := tmpl.Execute(c.Writer, gin.H{"users": users})
if err != nil {
panic(err)
}
})
router.Run(":8080")
fmt.Println("Server is running on port 8080")
}
type User struct {
Name string
Email string
}
func GetUsers() []User {
return []User{
{Name: "John Doe", Email: "[email protected]"},
{Name: "Alice Smith", Email: "[email protected]"},
}
}
We have added a route
/
to render the user list and provide a static list of users (to which we will add new users ahead).
That’s all. Restart the server and let’s visit — http://localhost:8080/ to check whether it renders the user list or not. It will render the user list as below.
user_row.html
<tr class="border border-gray-300">
<td class="px-4 py-2">{{ .Name }}</td>
<td class="px-4 py-2">{{ .Email }}</td>
<td class="px-4 py-2">
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
</td>
</tr>
P.S. — You will notice that it has the same structure as the user table row in users.html
. That’s because we want the same styling for the new row we are going to add.
<table></table>
tags.<form hx-post="/users" hx-target="#user-list" hx-swap="beforeend">
<input type="text" name="name" placeholder="Name" class="border border-gray-300 p-2 rounded">
<input type="email" name="email" placeholder="Email" class="border border-gray-300 p-2 rounded">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add User</button>
</form>
- hx-post=“/users” — When the form will be submitted, it will trigger post request to
/users
route.- hx-target=“#user-list” —Specify the target where we want to add data.
- hx-swap=“beforeend” — Specify the position where want to add data. In our case we want to add new user at the end of list, so we have used
beforeend
value.
/users
(POST) route in the main.go file.router.POST("/users", func(c *gin.Context) {
tmpl := template.Must(template.ParseFiles("user_row.html"))
name := c.PostForm("name")
email := c.PostForm("email")
user := User{Name: name, Email: email}
err := tmpl.Execute(c.Writer, user)
if err != nil {
panic(err)
}
})
It takes the name and email from the form input and executes the user_row.html.
Let’s try to add a new user to the table. Visit http://localhost:8080/ and click the Add User button.
Yayy! We’ve successfully added a new user to the list 🎉.
<button hx-delete="/users/{{ .Name }}" hx-target="closest tr" hx-confirm="Are you sure you want to delete this user?" class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
- hx-delete=“/users/{{ .Name }}” — It triggers
DELETE /users/:name
request. Of course, it’s not recommended way to delete something with the name, but here we have taken static users so we won’t have user_id available. That’s why we need to use name.- hx-target=“closest tr” — It detect the closest row and with given name and deletes it
- hx-confirm=“Are you sure you want to delete this user?” — It sets confirmation dialogue, used for dangerous actions.
router.DELETE("/users/:name", func(c *gin.Context) {
name := c.Param("name")
fmt.Println("Delete user with name:", name)
})
Here, I have just taken a name from the route param and printed it as we’re using static values for users.
But if we use real data, we need to manage user deletion from the database within this route.
The final code should look like this.
users.html
<!DOCTYPE html>
<html>
<head>
<title>Go + htmx app </title>
<script src="https://unpkg.com/[email protected]" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw" crossorigin="anonymous"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="text-center flex flex-col w-full gap-6 mt-10">
<form hx-post="/users" hx-target="#user-list" hx-swap="beforeend">
<input type="text" name="name" placeholder="Name" class="border border-gray-300 p-2 rounded">
<input type="email" name="email" placeholder="Email" class="border border-gray-300 p-2 rounded">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add User</button>
</form>
<table id="user-list" class="w-1/2 mx-auto mt-4 border border-gray-300">
<thead>
<tr class="border border-gray-300">
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Email</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
{{ range .users }}
<tr class="border border-gray-300">
<td class="px-4 py-2">{{ .Name }}</td>
<td class="px-4 py-2">{{ .Email }}</td>
<td class="px-4 py-2">
<button hx-delete="/users/{{ .Name }}" hx-target="closest tr" hx-confirm="Are you sure you want to delete this user?" class="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">Delete</button>
</td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>
main.go
package main
import (
"fmt"
"text/template"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
users := GetUsers()
tmpl := template.Must(template.ParseFiles("users.html"))
err := tmpl.Execute(c.Writer, gin.H{"users": users})
if err != nil {
panic(err)
}
})
router.POST("/users", func(c *gin.Context) {
tmpl := template.Must(template.ParseFiles("user_row.html"))
name := c.PostForm("name")
email := c.PostForm("email")
user := User{Name: name, Email: email}
err := tmpl.Execute(c.Writer, user)
if err != nil {
panic(err)
}
})
router.DELETE("/users/:name", func(c *gin.Context) {
name := c.Param("name")
fmt.Println("Delete user with name:", name)
})
router.Run(":8080")
fmt.Println("Server is running on port 8080")
}
type User struct {
Name string
Email string
}
func GetUsers() []User {
return []User{
{Name: "John Doe", Email: "[email protected]"},
{Name: "Alice Smith", Email: "[email protected]"},
}
}
Let’s see how it works.
Congratulations🎉!! We have learned how to use htmx in Golang!
While htmx is a powerful tool, it can sometimes feel a bit magical, especially for beginners. Here are some tips to make your htmx experiences more intuitive and efficient:
1. Clear and Concise Attribute Naming:
2. Leverage htmx’s Built-in Features:
3. Write Clear and Concise Server-Side Code:
However, these are only a few of the abilities, there are many more as we dig deep.