01a — Hello World Explicit

Same model as example 01, but with everything unbundled. Where 01 uses app.Run() (one call does everything), this example shows the explicit wiring: custom template, separate route handlers, and a service worker for the WASM build.


The model — unchanged

The model is identical to example 01. It lives in model.go and is shared by both server and WASM builds:

func model(app *lofigui.App) {
    lofigui.Print("Hello world.")
    for i := 0; i < 5; i++ {
        app.Sleep(1 * time.Second)
        lofigui.Printf("Count %d", i)
    }
    lofigui.Print("Done.")
}

Explicit routes — setupRoutes()

Instead of app.Run(), all routes are registered explicitly in setupRoutes(), which returns a standard *http.ServeMux:

func setupRoutes(app *lofigui.App) *http.ServeMux {
    mux := http.NewServeMux()

    // GET / — display current state
    mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
        app.HandleDisplay(w, r)
    })

    // POST /start — reset buffer, start model, redirect to /
    mux.HandleFunc("POST /start", func(w http.ResponseWriter, r *http.Request) {
        app.HandleRoot(w, r, model, true)
    })

    // POST /cancel — cancel running action, redirect to /
    mux.HandleFunc("POST /cancel", app.HandleCancel("/"))

    mux.HandleFunc("GET /favicon.ico", lofigui.ServeFavicon)
    return mux
}
HandleRoot vs Handle — Example 01 uses Handle() which auto-starts the model on the first request and auto-shuts down the server when done. Here, HandleRoot starts the model explicitly on POST /start, and HandleDisplay renders the current state on GET /. The model is started by a form submit, not by the first page visit.
HandleCancel — In example 01, Run() registers this internally. Here it's explicit: POST /cancel cancels the running action and redirects to /.

Custom template

The template lives at go/templates/hello.html and is pulled into the Go binary with //go:embed:

import _ "embed"

//go:embed templates/hello.html
var helloTemplate string

Embedding means the same binary works for server and WASM — WASM has no filesystem at runtime, so os.ReadFile wouldn't work, but bytes captured at build time do. The template itself is ordinary html/template:

<nav class="navbar is-primary">
  <div class="navbar-brand">
    <span class="navbar-item has-text-weight-bold">{{.version}}</span>
  </div>
  <div class="navbar-end">
    {{if eq .polling "Running"}}
      <span class="tag is-warning">Running</span>
      <form action="cancel" method="post">
        <button class="tag is-danger is-light" type="submit">Cancel</button>
      </form>
    {{else}}
      <span class="tag is-success">Ready</span>
    {{end}}
  </div>
</nav>
<section class="section">
  <div class="container content">
    {{.results}}
    {{if ne .polling "Running"}}
      <form action="start" method="post">
        <button class="button is-success" type="submit">Start</button>
      </form>
    {{end}}
  </div>
</section>
Form-based actions — Start and Cancel are HTML forms with method="post". No JavaScript needed. The template conditionally shows the Start button when idle and the Cancel button when running.
{{.polling}}"Running" while the model is active, "Stopped" when idle. Combined with the HTTP Refresh header (set by HandleDisplay), the page auto-refreshes while running and stops when done.

Server vs WASM: same handlers, different entry points

The key insight: setupRoutes() is called by both builds. Only the entry point differs.

Server (main.go)

func main() {
    app := lofigui.NewApp()
    app.Version = "Hello World Explicit v1.0"
    app.SetDisplayURL("/")

    ctrl, _ := lofigui.NewController(lofigui.ControllerConfig{
        TemplateString: helloTemplate,
        Name:           "Hello World Explicit",
    })
    app.SetController(ctrl)

    mux := setupRoutes(app)
    log.Fatal(http.ListenAndServe(":1341", mux))
}

WASM (main_wasm.go)

func main() {
    app := lofigui.NewApp()
    app.Version = "Hello World Explicit v1.0"
    app.SetDisplayURL("/")

    ctrl, _ := lofigui.NewController(lofigui.ControllerConfig{
        TemplateString: helloTemplate,
        Name:           "Hello World Explicit",
    })
    app.SetController(ctrl)

    mux := setupRoutes(app)
    wasmhttp.Serve(mux)  // ← only this line differs
}
wasmhttp.Serve(mux) replaces http.ListenAndServe(). The go-wasm-http-server library registers a service worker that intercepts browser fetch events and routes them through the Go *http.ServeMux running inside WASM. The browser makes real HTTP requests; the service worker answers them from Go.

JavaScript: direct exports vs service worker

This is the fundamental difference between example 01 and 01a's WASM builds:

Example 01 — direct syscall/js exports (app.js)

// Go exports functions to JavaScript via RunWASM():
//   goStart(), goRender(), goCancel(), goIsRunning()

// JavaScript actively polls Go for updates:
renderInterval = setInterval(function() {
    outputDiv.innerHTML = goRender();    // pull HTML from Go
    updateStatus();                       // check goIsRunning()
}, 500);

// Button clicks call Go directly:
startBtn.addEventListener('click', function() { goStart(); });
cancelBtn.addEventListener('click', function() { goCancel(); });
01 pattern: JavaScript is the driver. It calls Go functions on a timer, manages button state, and renders HTML from Go's buffer. The page never makes HTTP requests — everything happens through direct function calls between JS and WASM.

Example 01a — service worker (go/templates/sw/)

The sw/ subdirectory holds the whole SW integration — each file is hand-written and visible. The sw.js vendors the go-wasm-http-server runtime as a local wasmhttp_sw.js (not fetched from a CDN) so that Firefox's strict CSP and Enhanced Tracking Protection can't block it:

// sw.js
importScripts('wasm_exec.js');
importScripts('wasmhttp_sw.js');   // local copy, not CDN

self.addEventListener('install',  () => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(clients.claim()));

registerWasmHTTPListener('main.wasm', {
    passthrough: function(request) {
        var url = new URL(request.url);
        if (url.hostname !== self.location.hostname) return true;
        if (url.pathname.endsWith('/index.html')) return true;
        return false;
    }
});
01a pattern: The service worker is the driver. It loads the Go WASM binary and intercepts all fetch events. When the browser requests /, the service worker routes it to Go's HandleDisplay. When a form POSTs to /start, the service worker routes it to Go's HandleRoot. No custom JavaScript, no polling logic, no button state management — just standard HTML forms and HTTP semantics.

When to use which

Approach Best for Trade-off
Direct exports (01) Simple apps, single-page output, no routing Requires custom JS per app; JS manages all state
Service worker (01a) Multi-route apps, forms, HTMX Requires service worker support; slightly larger binary

The service worker approach scales to complex apps (examples 09-12) because adding a new route is just adding a handler in setupRoutes() — no JS changes needed. The direct export approach stays simpler for single-page demos where you just need goStart() and goRender().


Running

# Server mode
task go-example:01a
# Opens http://localhost:1341 — click Start, watch output, click Cancel

# WASM demo (via docs)
task docs:build-wasm
tp pages
# Navigate to http://localhost:8080/01a_hello_world_explicit/wasm_demo/sw/