Muxt facilitates improved locality of behavior by letting you register HTTP routes in your templates
This is especially helpful when you are writing HTMX.
Given main.go and index.gohtml, muxt will generate template_routes.go.
index.gohtml
<!DOCTYPE html>
<html lang="en">
{{block "head" "example"}}
<head>
<meta charset='UTF-8'/>
<title>{{.}}</title>
<script src='https://unpkg.com/htmx.org@2.0.1' integrity='sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/' crossorigin='anonymous'></script>
<script src='https://unpkg.com/htmx-ext-response-targets@2.0.0/response-targets.js'></script>
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css'>
</head>
{{end}}
<body hx-ext='response-targets'>
<main class='container'>
<table>
<thead>
<tr>
<th>Fruit</th>
<th>Count</th>
</tr>
</thead>
<tbody hx-target="closest tr" hx-swap="outerHTML">
{{- define "fruit row" -}}
<tr>
<td>{{ .Name }}</td>
<td id="count" hx-get='/fruits/{{.Name}}/edit'>{{ .Value }}</td>
</tr>
{{- end -}}
{{range .}}
{{template "fruit row" .}}
{{end}}
{{- define "GET /{$} List(ctx)" -}}
{{template "index.gohtml" .}}
{{- end -}}
{{- define "GET /fruits/{fruit}/edit GetFormEditRow(fruit)" -}}
<tr>
<td>{{ .Row.Name }}</td>
<td>
<form hx-patch='/fruits/{{.Row.Name}}'>
<input aria-label='Count' type='number' name='count' value='{{ .Row.Value }}' step='1' min='0'>
<input type='submit' value='Update'>
</form>
<p id='error'>{{.Error}}</p>
</td>
</tr>
{{- end -}}
{{- define "PATCH /fruits/{fruit} SubmitFormEditRow(request, fruit)" }}
{{- if .Error -}}
{{template "GET /fruits/{fruit}/edit GetFormEditRow(fruit)" .}}
{{- else -}}
{{template "fruit row" .Row}}
{{- end -}}
{{ end -}}
</tbody>
</table>
</main>
</body>
</html>
{{define "GET /help"}}
<!DOCTYPE html>
<html lang='us-en'>
{{template "head" "Help"}}
<body>
<main class='container'>
Hello, help!
</main>
</body>
</html>
{{end}}
main.go
In your Go code focus on your domain. Let muxt generate handlers and wire them up with the HTTP mux (multiplexer).
package main
import (
"context"
"embed"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
)
//go:embed *.gohtml
var templateSource embed.FS
var templates = template.Must(template.ParseFS(templateSource, "*"))
type Backend struct {
data []Row
}
type EditRowPage struct {
Row Row
Error error
}
func (b *Backend) SubmitFormEditRow(request *http.Request, fruit string) EditRowPage {
count, err := strconv.Atoi(request.FormValue("count"))
if err != nil {
return EditRowPage{Error: err, Row: Row{Name: fruit}}
}
for i := range b.data {
if b.data[i].Name == fruit {
b.data[i].Value = count
return EditRowPage{Error: nil, Row: b.data[i]}
}
}
return EditRowPage{Error: fmt.Errorf("fruit not found")}
}
func (b *Backend) GetFormEditRow(fruit string) EditRowPage {
for i := range b.data {
if b.data[i].Name == fruit {
return EditRowPage{Error: nil, Row: b.data[i]}
}
}
return EditRowPage{Error: fmt.Errorf("fruit not found")}
}
type Row struct {
Name string
Value int
}
func (b *Backend) List(_ context.Context) []Row { return b.data }
//go:generate muxt generate --receiver Backend
func main() {
backend := &Backend{
data: []Row{
{Name: "Peach", Value: 10},
{Name: "Plum", Value: 20},
{Name: "Pineapple", Value: 2},
},
}
mux := http.NewServeMux()
Routes(mux, backend)
log.Fatal(http.ListenAndServe(":8080", mux))
}
template_routes.go
This file is generated by running go generate
in the same directory as main.go
// Code generated by muxt. DO NOT EDIT.
package main
import (
"context"
"net/http"
"bytes"
"html/template"
)
type RoutesReceiver interface {
SubmitFormEditRow(request *http.Request, fruit string) EditRowPage
GetFormEditRow(fruit string) EditRowPage
List(_ context.Context) []Row
}
func Routes(mux *http.ServeMux, receiver RoutesReceiver) {
mux.HandleFunc("PATCH /fruits/{fruit}", func(response http.ResponseWriter, request *http.Request) {
fruit := request.PathValue("fruit")
data := receiver.SubmitFormEditRow(request, fruit)
execute(response, request, templates.Lookup("PATCH /fruits/{fruit} SubmitFormEditRow(request, fruit)"), http.StatusOK, data)
})
mux.HandleFunc("GET /fruits/{fruit}/edit", func(response http.ResponseWriter, request *http.Request) {
fruit := request.PathValue("fruit")
data := receiver.GetFormEditRow(fruit)
execute(response, request, templates.Lookup("GET /fruits/{fruit}/edit GetFormEditRow(fruit)"), http.StatusOK, data)
})
mux.HandleFunc("GET /help", func(response http.ResponseWriter, request *http.Request) {
execute(response, request, templates.Lookup("GET /help"), http.StatusOK, request)
})
mux.HandleFunc("GET /{$}", func(response http.ResponseWriter, request *http.Request) {
ctx := request.Context()
data := receiver.List(ctx)
execute(response, request, templates.Lookup("GET /{$} List(ctx)"), http.StatusOK, data)
})
}
func execute(response http.ResponseWriter, request *http.Request, t *template.Template, code int, data any) {
buf := bytes.NewBuffer(nil)
if err := t.Execute(buf, data); err != nil {
http.Error(response, err.Error(), http.StatusOK)
return
}
response.WriteHeader(code)
_, _ = buf.WriteTo(response)
}