Files
ersteller/htmx_route.go
T
2025-08-13 14:59:28 +02:00

225 lines
5.9 KiB
Go

package ersteller_lib
import (
"encoding/json"
"fmt"
"maragu.dev/gomponents"
htmx "maragu.dev/gomponents-htmx"
"net/http"
"strings"
)
type HtmxContext interface {
Render(node gomponents.Node)
SetError(err error)
GetLanguage() Language
GetActivePath() *ActivePath
HasActivePath() bool
SetActivePath(activePath *ActivePath)
GetPath() string
GetPathParam(key string, defaultValue ...string) string
GetFormValue(key string) string
SetHeader(key string, value string)
}
type HtmxContextImpl struct {
req *http.Request
res http.ResponseWriter
language Language
activePath *ActivePath
}
func (c *HtmxContextImpl) SetHeader(key string, value string) {
c.res.Header().Set(key, value)
}
func (c *HtmxContextImpl) GetFormValue(key string) string {
err := c.req.ParseForm()
if err != nil {
Error("failed to parse form", err)
c.SetError(err)
return ""
}
return c.req.FormValue(key)
}
func (c *HtmxContextImpl) SetError(err error) {
_, writeErr := c.res.Write([]byte(err.Error()))
if writeErr != nil {
Error("failed to write error", writeErr)
}
c.res.WriteHeader(500)
}
func (c *HtmxContextImpl) GetPathParam(key string, defaultValue ...string) string {
pathParam := c.req.PathValue(key)
if pathParam != "" {
return pathParam
}
if len(defaultValue) > 0 {
return defaultValue[0]
}
return ""
}
func (c *HtmxContextImpl) GetPath() string {
return c.req.URL.Path
}
func (c *HtmxContextImpl) SetActivePath(activePath *ActivePath) {
c.activePath = activePath
}
func (c *HtmxContextImpl) GetActivePath() *ActivePath {
return c.activePath
}
func (c *HtmxContextImpl) HasActivePath() bool {
return c.activePath != nil
}
func (c *HtmxContextImpl) GetLanguage() Language {
return c.language
}
func NewHtmxContext(req *http.Request, res http.ResponseWriter, language Language) HtmxContext {
return &HtmxContextImpl{req: req, res: res, language: language}
}
func (c *HtmxContextImpl) Render(node gomponents.Node) {
err := node.Render(c.res)
if err != nil {
Error("failed to render", err)
c.SetError(err)
}
}
type HtmxRouteFunc func(ctx HtmxContext)
type HtmxPathParam struct {
Key string
Value string
}
type LocationRedirectParams struct {
Path string `json:"path"`
Target string `json:"target,omitempty"`
Swap string `json:"swap,omitempty"`
}
type HtmxRoute interface {
WithPathParams(params ...HtmxPathParam) HtmxRoute
ToUrl(language Language, queryParams ...HtmxPathParam) string
GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node
SetActivePath(activePath *ActivePath) HtmxRoute
Add(server *http.ServeMux)
Execute(c HtmxContext)
RedirectToThisRoute(c HtmxContext, params LocationRedirectParams)
}
func addLanguageToPath(path string, language Language) string {
return "/" + string(language) + path
}
type CommonHtmxRoute struct {
Paths LanguagePaths
RouteFunc HtmxRouteFunc
PathParams []HtmxPathParam
ActivePath *ActivePath
Method string
HtmxMethod HtmxHttpMethodFunction
}
func (h CommonHtmxRoute) RedirectToThisRoute(c HtmxContext, params LocationRedirectParams) {
params.Path = h.ToUrl(c.GetLanguage())
jsonData, err := json.Marshal(params)
if err != nil {
marshalError := fmt.Errorf("failed to marshal LocationRedirectParams: %w, %w", err, params)
Error(marshalError)
c.SetError(marshalError)
return
}
c.SetHeader("HX-Location", string(jsonData))
}
func NewCommonHtmxRoute(routeFunc HtmxRouteFunc, paths LanguagePaths, method string,
htmxMethod HtmxHttpMethodFunction) *CommonHtmxRoute {
return &CommonHtmxRoute{RouteFunc: routeFunc, Paths: paths, Method: method, HtmxMethod: htmxMethod}
}
func (h CommonHtmxRoute) Execute(c HtmxContext) {
h.RouteFunc(c)
}
func (h CommonHtmxRoute) SetActivePath(activePath *ActivePath) HtmxRoute {
h.ActivePath = activePath
return h
}
func (h CommonHtmxRoute) AddWithMethod(method string, server *http.ServeMux) {
for language, path := range h.Paths {
server.HandleFunc(method+" "+addLanguageToPath(path, language), func(res http.ResponseWriter, req *http.Request) {
context := NewHtmxContext(req, res, language)
if h.ActivePath != nil {
context.SetActivePath(h.ActivePath)
}
h.RouteFunc(context)
})
}
}
func (h CommonHtmxRoute) Add(server *http.ServeMux) {
h.AddWithMethod(h.Method, server)
}
func (h CommonHtmxRoute) WithPathParams(params ...HtmxPathParam) HtmxRoute {
h.PathParams = params
return h
}
func (h CommonHtmxRoute) ToUrl(language Language, queryParams ...HtmxPathParam) string {
path := addLanguageToPath(h.Paths[language], language)
if len(h.PathParams) == 0 {
return path
}
for _, param := range h.PathParams {
path = strings.ReplaceAll(path, "{"+param.Key+"}", param.Value)
}
return path
}
type HtmxHttpMethodFunction func(url string) gomponents.Node
func (h CommonHtmxRoute) GetHtmxFromMethod(method HtmxHttpMethodFunction, language Language, queryParams ...HtmxPathParam) gomponents.Node {
return method(h.ToUrl(language, queryParams...))
}
func (h CommonHtmxRoute) GetHtmx(language Language, queryParams ...HtmxPathParam) gomponents.Node {
return h.GetHtmxFromMethod(h.HtmxMethod, language, queryParams...)
}
func NewHtmxGetRoute(routeFunc HtmxRouteFunc, paths LanguagePaths) HtmxRoute {
return NewCommonHtmxRoute(routeFunc, paths, "GET", htmx.Get)
}
func NewHtmxPostRoute(routeFunc HtmxRouteFunc, paths LanguagePaths) HtmxRoute {
return NewCommonHtmxRoute(routeFunc, paths, "POST", htmx.Post)
}
func HandleStaticAndDefaultPath(server *http.ServeMux, defaultRoute HtmxRoute, defaultLanguage Language) func(http.ResponseWriter, *http.Request) {
staticHandler := http.StripPrefix("/static", http.FileServer(http.Dir("./static")))
server.Handle("GET /static", staticHandler)
return func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasPrefix(path, "/static") {
staticHandler.ServeHTTP(w, r)
return
}
if path == "/" {
defaultRoute.Execute(NewHtmxContext(r, w, defaultLanguage))
return
}
http.Error(w, "Not found", http.StatusNotFound)
}
}