DEV Community

Chase Fleming
Chase Fleming

Posted on • Edited on

Building a Counter App with htmx, Go Fiber, and elem-go

In this tutorial, we'll build a simple counter application using Go Fiber, htmx, and the elem-go library for type-safe HTML templating.

Explaining the Dependencies

We'll use three primary packages to build the app:

  • Go Fiber: An HTTP web server implementation inspired by Express.js's design in Node.js. Go Fiber facilitates the development of speedy and scalable web applications in Go.
  • htmx: Provides direct access to AJAX, WebSockets, and Server-Sent Events in HTML, simplifying dynamic updates of web pages without full reloads.
  • elem-go: A nimble Go library designed for programmatic and type-safe HTML element creation and management. It ensures you're working with valid HTML elements, attributes, and styles, helping to minimize potential runtime errors.

Setting Up the Project

Start by initializing a new Go project:

mkdir go-htmx-counter
cd go-htmx-counter
go mod init github.com/yourusername/go-htmx-counter
Enter fullscreen mode Exit fullscreen mode

Next, install the required packages:

go get github.com/chasefleming/elem-go
go get github.com/gofiber/fiber/v2
Enter fullscreen mode Exit fullscreen mode

Setting Up the Go Fiber Server

Create a new file named main.go. Here, set up a root route using Go Fiber, which listens on port 3000 and serves the "Hello, World!" message:

package main

import (
    "github.com/gofiber/fiber/v2"
)

func main() {
    app := fiber.New()

    app.Get("/", func(c *fiber.Ctx) error {
        c.Type("html")
        return c.SendString("Hello, world!")
    })

    app.Listen(":3000")
}
Enter fullscreen mode Exit fullscreen mode

To start your server:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:3000, and you should be greeted by "Hello, world!".

Importing Subpackages from elem-go

Once you have installed the elem-go package, you also gain access to its subpackages, which are designed to enhance your development experience with type-safe utilities for attributes, styles, and htmx integration. Here’s how you can import them into your project:

import (
    //...[existing imports]...
    "github.com/chasefleming/elem-go/attrs"
    "github.com/chasefleming/elem-go/styles"
    "github.com/chasefleming/elem-go/htmx"
)
Enter fullscreen mode Exit fullscreen mode

Make sure to include these imports in your main.go file or any other file where you are using the elem-go library. By importing these subpackages, you gain access to a suite of constants and helper functions that facilitate the creation of type-safe HTML elements. This structure helps prevent common mistakes such as typos in attribute names and ensures that style properties are correctly applied to your elements.

Each subpackage serves a specific purpose:

  • attrs: This subpackage contains constants for HTML attribute names, ensuring that you use valid attribute keys when creating elements.
  • styles: It provides constants for CSS property names, allowing you to apply styles with the correct property keys.
  • htmx: This subpackage includes constants for htmx-specific attributes, making it easier to work with htmx in your templates.

Create the Counter Page using elem-go

Begin by adding the script tag for the htmx source to our app's <head>. With elem-go, you can generate this HTML in a type-safe manner. Using elem. followed by the tag name lets you create the desired elements. The first argument in an elem-go element defines its attributes, while subsequent arguments denote child elements:

app.Get("/", func(c *fiber.Ctx) error {
    head := elem.Head(nil, elem.Script(attrs.Props{attrs.Src: "https://unpkg.com/htmx.org@1.9.6"}))
    //...[rest of the code]...
})

Enter fullscreen mode Exit fullscreen mode

With elem-go, you can also utilize type-safe constants for both attribute and style keys. To define the styles, add the following after head:

bodyStyle := styles.Props{  
    styles.BackgroundColor: "#f4f4f4",
    styles.FontFamily:      "Arial, sans-serif",
    styles.Height:          "100vh",
    styles.Display:         "flex",
    styles.FlexDirection:   "column",
    styles.AlignItems:      "center",
    styles.JustifyContent:  "center",
}

buttonStyle := styles.Props{
    styles.Padding:       "10px 20px",
    styles.BackgroundColor: "#007BFF",
    styles.Color:         "#fff",
    styles.BorderColor:   "#007BFF",
    styles.BorderRadius:  "5px",
    styles.Margin:        "10px",
    styles.Cursor:        "pointer",
}
Enter fullscreen mode Exit fullscreen mode

After defining the styles, we'll construct our body elements using the methods provided by the elem-go library. Additionally, the htmx subpackage we imported offers specific values, which are accessible with the htmx. prefix:

body := elem.Body(
    attrs.Props{
        attrs.Style: bodyStyle.ToInline(),
    },
    elem.H1(nil, elem.Text("Counter App")),
    elem.Div(attrs.Props{attrs.ID: "count"}, elem.Text("0")),
    elem.Button(
        attrs.Props{
            htmx.HXPost:   "/increment",
            htmx.HXTarget: "#count",
            attrs.Style:   buttonStyle.ToInline(),
        }, 
        elem.Text("+")
    ),
    elem.Button(
        attrs.Props{
            htmx.HXPost:   "/decrement",
            htmx.HXTarget: "#count",
            attrs.Style:   buttonStyle.ToInline(),
        }, 
        elem.Text("-")
    ),
)
Enter fullscreen mode Exit fullscreen mode

Once the body content is defined, encapsulate it within the <html> tag using the elem.Html method followed by Render. Append this after defining the body:

pageContent := elem.Html(nil, head, body)  

html := pageContent.Render()
Enter fullscreen mode Exit fullscreen mode

Adding Interactivity with htmx

Next, we'll establish handlers for the /increment and /decrement endpoints. These are invoked via htmx.HXPost, the programmatic equivalent of the hx-post attribute in HTML. To do this, integrate the following into your main function after initializing the app:

var count int

app.Post("/increment", func(c *fiber.Ctx) error {
    count++
    return c.SendString(fmt.Sprintf("%d", count))
})

app.Post("/decrement", func(c *fiber.Ctx) error {
    count--
    return c.SendString(fmt.Sprintf("%d", count))
})
Enter fullscreen mode Exit fullscreen mode

The above routes will update and then send back the current value of the counter. Ensure you have imported the fmt package at the top of your file to use the Sprintf function.

Running the Counter App

Run the server, navigate to http://localhost:3000, and you'll be greeted with the Counter App. The "+" and "-" buttons will now seamlessly update the count value without a full page refresh.

Congratulations on successfully building a basic counter app using Go Fiber for the backend, htmx for frontend interactivity, and elem-go for type-safe HTML templating. Expand on this foundation for more advanced projects!

If you'd like to see the full code, check out this repo.

Top comments (1)

Collapse
 
gedw99 profile image
Gerard Webb

Nice work Chase.

github.com/anycable/anycable-go looks like a nice match with go-elem. It's designed for Turbo, but it will work with go-elem and htmx and SSE / Web Sockets.

It can also embed the NATS Server for you.