Greeting App Example
This example demonstrates form handling with HTMX, dynamic values via
hx-vals, and server-side body parsing.
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 = "127.0.0.1",
port = 3000,
})
local names = {}
app:get("/", function(ctx)
local list_items = ""
for _, name in ipairs(names) do
list_items = list_items .. Nixi.li(nil, Nixi.escape(name))
end
local name_list = #names > 0
and Nixi.ul({ id = "name-list" }, list_items)
or Nixi.p({ id = "name-list" }, "No greetings yet. Add one below!")
return Nixi.Layout({
title = "Greeting App",
body = Nixi.div(
{ style = "max-width: 600px; margin: 50px auto; padding: 20px;" },
Nixi.h1(nil, "Greeting App"),
Nixi.p(nil, "Enter your name to receive a personalized greeting!"),
Nixi.div(
{ id = "name-list-container" },
name_list
),
Nixi.div(
{ style = "margin-top: 30px; padding: 20px; background: #1a1a2e; border-radius: 12px;" },
Nixi.h3(nil, "Add Your Greeting"),
Nixi.form(
{
["hx-post"] = "/greet",
["hx-target"] = "#name-list-container",
["hx-swap"] = "innerHTML"
},
Nixi.div(
{ style = "display: flex; gap: 10px; flex-wrap: wrap;" },
Nixi.input({
type = "text",
name = "name",
placeholder = "Your name",
required = true,
style = "flex: 1; padding: 12px; border-radius: 8px; border: 1px solid #2d2d44; background: #0f0f1a; color: #e8e8e8;"
}),
Nixi.input({
type = "number",
name = "age",
placeholder = "Age",
min = "1",
max = "150",
style = "width: 100px; padding: 12px; border-radius: 8px; border: 1px solid #2d2d44; background: #0f0f1a; color: #e8e8e8;"
}),
Nixi.button({
type = "submit",
class = "btn primary",
style = "padding: 12px 24px; background: #00ff88; color: #1a1a2e; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;"
}, "Greet Me!")
)
)
),
Nixi.div(
{ id = "message", style = "margin-top: 20px; text-align: center; min-height: 40px;" }
)
),
head = [[
<style>
.btn { transition: all 0.2s; }
.btn:hover { transform: scale(1.02); }
.btn:active { transform: scale(0.98); }
#name-list li {
padding: 12px;
background: #1a1a2e;
margin: 8px 0;
border-radius: 8px;
list-style: none;
}
</style>
]]
})
end)
app:post("/greet", function(ctx)
local body = ctx.request.body
local name = body.name or "Anonymous"
local age = body.age
local greeting = string.format("Hello, %s!", name)
if age then
greeting = greeting .. string.format(" You are %s years old.", age)
end
table.insert(names, name .. " (" .. greeting .. ")")
local list_items = ""
for _, entry in ipairs(names) do
list_items = list_items .. Nixi.li(nil, Nixi.escape(entry))
end
local name_list = #names > 0
and Nixi.ul({ id = "name-list" }, list_items)
or Nixi.p({ id = "name-list" }, "No greetings yet. Add one below!")
return Nixi.div(
{ id = "name-list-container" },
name_list,
Nixi.div(
{ style = "margin-top: 20px; padding: 20px; background: #1a1a2e; border-radius: 12px;" },
Nixi.h3(nil, "Add Your Greeting"),
Nixi.form(
{
["hx-post"] = "/greet",
["hx-target"] = "#name-list-container",
["hx-swap"] = "innerHTML"
},
Nixi.div(
{ style = "display: flex; gap: 10px; flex-wrap: wrap;" },
Nixi.input({
type = "text",
name = "name",
placeholder = "Your name",
required = true,
style = "flex: 1; padding: 12px; border-radius: 8px; border: 1px solid #2d2d44; background: #0f0f1a; color: #e8e8e8;"
}),
Nixi.input({
type = "number",
name = "age",
placeholder = "Age",
min = "1",
max = "150",
style = "width: 100px; padding: 12px; border-radius: 8px; border: 1px solid #2d2d44; background: #0f0f1a; color: #e8e8e8;"
}),
Nixi.button({
type = "submit",
class = "btn primary",
style = "padding: 12px 24px; background: #00ff88; color: #1a1a2e; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;"
}, "Greet Me!")
)
)
)
)
end)
return app
Form Handling with HTMX
The form uses HTMX attributes to submit data without page reload:
Nixi.form(
{
["hx-post"] = "/greet", -- POST to this endpoint
["hx-target"] = "#container", -- Replace this element
["hx-swap"] = "innerHTML" -- Swap method
},
-- Form fields
)
hx-vals for Dynamic Values
Use hx-vals to send additional JavaScript-evaluated
values:
Nixi.form(
{
["hx-post"] = "/greet",
["hx-vals"] = '{"timestamp":"js:new Date().toISOString()"}'
},
-- fields
)
Available value sources:
js:expression- Evaluate JavaScript expressionjson:data- Include JSON objecttruthy:value- Include if truthy
Body Parsing
Nixi automatically parses request bodies. Access form data via
ctx.request.body:
app:post("/greet", function(ctx)
-- Body is auto-parsed as key-value table
local name = ctx.request.body.name
local age = ctx.request.body.age
return Nixi.p(nil, "Hello " .. name)
end)
| Content-Type | Parsing |
|---|---|
application/x-www-form-urlencoded |
Key-value pairs |
application/json |
Parsed JSON table |
multipart/form-data |
Fields + file info |
Request Flow
┌─────────────────────────────────────────────────────────────────┐
│ GREETING APP FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. User visits "/" │
│ └─> GET / returns full page with form │
│ │
│ 2. User fills form and submits │
│ └─> POST /greet with form data │
│ │
│ 3. Server processes: │
│ ├─ Parse body (name, age) │
│ ├─ Generate greeting message │
│ ├─ Update names table │
│ └─ Return updated list HTML │
│ │
│ 4. HTMX swaps content │
│ └─ #name-list-container updated with new list │
│ │
│ 5. State preserved server-side │
│ └─ names table persists until server restart │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Concepts
| Feature | Implementation |
|---|---|
| Form Submission | hx-post, hx-target |
| Dynamic Values | hx-vals with js: prefix |
| Body Parsing | ctx.request.body |
| Partial Updates | Return only updated container |
| State Management | Server-side names table |