soma.cell

HTMX Counter

A counter that updates without page reloads using HTMX. Serve: soma serve 054_htmx_counter.cell Test: open http://localhost:8080

Example 054 of the basic Soma programs: a small, commented program that teaches one thing. Every output below was printed by soma 3.2.4 when this page was built.

How it is built

The cell HtmxCounter declares 5 signals in its face: request(method: String, path: String, body: String) -> String, increment() -> String, decrement() -> String, value() -> String and demo() -> String; keeps 1 memory slot: data (Map<String, Int>, persistent and consistent). Handlers: request (line 28), value (line 49), increment (line 54), decrement (line 61) and demo (line 68).

Generated from soma describe on this file.

The source

054_htmx_counter.cell

// ============================================================================
// 054 — HTMX Counter
// ============================================================================
// A counter that updates without page reloads using HTMX.
//
// Serve: soma serve 054_htmx_counter.cell
// Test:  open http://localhost:8080
//
// Run:   soma run 054_htmx_counter.cell demo
// Expect:
//   Count: 0
//   Count: 1
//   Count: 2

cell HtmxCounter {
    face {
        signal request(method: String, path: String, body: String) -> String
        signal increment() -> String
        signal decrement() -> String
        signal value() -> String
        signal demo() -> String
    }

    memory {
        data: Map<String, Int> [persistent, consistent]
    }

    on request(method: String, path: String, body: String) {
        return html("<!DOCTYPE html>
<html>
<head>
    <title>HTMX Counter</title>
    <script src='https://unpkg.com/[email protected]'></script>
    <style>
        body { font-family: system-ui; text-align: center; padding: 50px; }
        .counter { font-size: 4rem; font-weight: bold; margin: 20px; }
        button { font-size: 1.5rem; padding: 10px 20px; margin: 5px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>HTMX Counter</h1>
    <div class='counter' id='count' hx-get='/value' hx-trigger='load'>0</div>
    <button hx-post='/increment' hx-target='#count'>+</button>
    <button hx-post='/decrement' hx-target='#count'>-</button>
</body>
</html>")
    }

    on value() {
        let v = data.get("count") ?? 0
        return html(to_string(v))
    }

    on increment() {
        let v = data.get("count") ?? 0
        data.set("count", v + 1)
        let result = v + 1
        return html(to_string(result))
    }

    on decrement() {
        let v = data.get("count") ?? 0
        data.set("count", v - 1)
        let result = v - 1
        return html(to_string(result))
    }

    on demo() {
        data.set("count", 0)
        let v0 = data.get("count")
        print("Count: {v0}")

        increment()
        let v1 = data.get("count")
        print("Count: {v1}")

        increment()
        let v2 = data.get("count")
        print("Count: {v2}")

        return "done"
    }
}

What the toolchain says

$ soma check 054_htmx_counter.cell
✓ All checks passed.
$ soma run 054_htmx_counter.cell demo
Count: 0
error: increment(): the face declares `-> String` but the handler returned Map {"_status": 200, "_body": "1", "_content
  --> 054_htmx_counter.cell:58:16
   |
58 |         return html(to_string(result))
   |                ^