Counter Example

This example demonstrates the core Nixi + HTMX pattern: a counter that updates without page reloads.

Complete Code

#!/usr/bin/env lua
---@meta

package.path = "src/?.lua;src/?/init.lua;" .. package.path

local Nixi = require("nixi.init")

local app = Nixi.new({
    host = "0.0.0.0",
    port = 8080,
})

app:get("/", function(ctx)
    return Nixi.Layout({
        title = "Nixi Counter",
        body = Nixi.div(
            { style = "text-align: center; padding: 50px;" },
            Nixi.h1(nil, "Nixi Counter Demo"),
            Nixi.p(nil, "Count: "),
            Nixi.span({id = "count"}, "0"),
            Nixi.div(
                { style = "margin-top: 20px; display: flex; gap: 10px; justify-content: center;" },
                Nixi.button(
                    {
                        ["hx-get"] = "/decrement",
                        ["hx-target"] = "#count",
                        ["hx-swap"] = "innerHTML",
                        class = "btn"
                    },
                    "-"
                ),
                Nixi.button(
                    {
                        ["hx-get"] = "/increment",
                        ["hx-target"] = "#count",
                        ["hx-swap"] = "innerHTML",
                        class = "btn primary"
                    },
                    "+"
                )
            ),
            Nixi.div(
                { style = "margin-top: 20px;" },
                Nixi.button({
                    ["hx-get"] = "/reset",
                    ["hx-target"] = "#count",
                    ["hx-swap"] = "innerHTML",
                    class = "btn danger"
                }, "Reset")
            )
        ),
        head = [[
            <style>
                .btn {
                    padding: 12px 24px;
                    font-size: 20px;
                    border: none;
                    border-radius: 8px;
                    cursor: pointer;
                    transition: all 0.2s;
                    font-weight: bold;
                }
                .btn:hover { transform: scale(1.05); }
                .btn:active { transform: scale(0.95); }
                .primary { background: #00ff88; color: #1a1a2e; }
                .danger { background: #f44336; color: white; }
                #count {
                    font-size: 32px;
                    font-weight: bold;
                    display: inline-block;
                    min-width: 60px;
                }
            </style>
        ]]
    })
end)

app:get("/increment", function(ctx)
    return Nixi.span({class = "nixi-pulse"}, "1")
end)

app:get("/decrement", function(ctx)
    return Nixi.span({class = "nixi-pulse"}, "-1")
end)

app:get("/reset", function(ctx)
    return Nixi.span(nil, "0")
end)

print("App loaded")
return app

How It Works

1. Main Page Route

The main route returns a complete HTML page with:

2. HTMX Button Attributes

Nixi.button(
    {
        ["hx-get"] = "/increment",    -- URL to request
        ["hx-target"] = "#count",    -- Element to update
        ["hx-swap"] = "innerHTML"   -- How to update
    },
    "+"
)

What happens when clicked:

  1. Browser sends GET request to /increment
  2. Server returns new count as HTML
  3. HTMX replaces content of #count element

3. Update Endpoints

app:get("/increment", function(ctx)
    count = count + 1
    return Nixi.span({ id = "count" }, tostring(count))
end)

Each endpoint modifies the server-side state and returns only the HTML fragment to update.

Key Concepts Demonstrated

Concept Implementation
Server State local count = 0
HTMX Attributes hx-get, hx-target, hx-swap
Element Targeting id = "count" and #count
Partial Response Returns only span, not full page