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:

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