setup service locator
start making sample todo app
This commit is contained in:
parent
f0f979e3a2
commit
f0a38379c3
13 changed files with 168 additions and 41 deletions
|
|
@ -187,7 +187,7 @@ func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
||||||
fName := "GetPartialFromContext"
|
fName := "GetPartialFromContext"
|
||||||
|
|
||||||
body := `
|
body := `
|
||||||
path := ctx.Path()
|
path := ctx.Request().URL.Path
|
||||||
`
|
`
|
||||||
|
|
||||||
moduleName := GetModuleName()
|
moduleName := GetModuleName()
|
||||||
|
|
@ -200,7 +200,8 @@ func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
||||||
|
|
||||||
body += fmt.Sprintf(`
|
body += fmt.Sprintf(`
|
||||||
if path == "%s" || path == "%s" {
|
if path == "%s" || path == "%s" {
|
||||||
return %s(ctx)
|
cc := ctx.(*h.RequestContext)
|
||||||
|
return %s(cc)
|
||||||
}
|
}
|
||||||
`, f.FuncName, path, caller)
|
`, f.FuncName, path, caller)
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +321,8 @@ func writePagesFile() {
|
||||||
|
|
||||||
body += fmt.Sprintf(`
|
body += fmt.Sprintf(`
|
||||||
f.GET("%s", func(ctx echo.Context) error {
|
f.GET("%s", func(ctx echo.Context) error {
|
||||||
return h.HtmlView(ctx, %s(ctx))
|
cc := ctx.(*h.RequestContext)
|
||||||
|
return h.HtmlView(ctx, %s(cc))
|
||||||
})
|
})
|
||||||
`, formatRoute(page.Path), call)
|
`, formatRoute(page.Path), call)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ func DownloadTemplate(outPath string) {
|
||||||
fmt.Sprintf("module %s", templateName),
|
fmt.Sprintf("module %s", templateName),
|
||||||
fmt.Sprintf("module %s", newModuleName))
|
fmt.Sprintf("module %s", newModuleName))
|
||||||
|
|
||||||
|
_ = util.ReplaceTextInDirRecursive(newDir, templateName, newModuleName, func(file string) bool {
|
||||||
|
return strings.HasSuffix(file, ".go")
|
||||||
|
})
|
||||||
|
|
||||||
fmt.Printf("Setting up the project in %s\n", newDir)
|
fmt.Printf("Setting up the project in %s\n", newDir)
|
||||||
process.SetWorkingDir(newDir)
|
process.SetWorkingDir(newDir)
|
||||||
run.Setup()
|
run.Setup()
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ func Setup() {
|
||||||
copyassets.CopyAssets()
|
copyassets.CopyAssets()
|
||||||
_ = astgen.GenAst(true)
|
_ = astgen.GenAst(true)
|
||||||
_ = css.GenerateCss(true)
|
_ = css.GenerateCss(true)
|
||||||
|
EntGenerate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,3 +16,12 @@ func ReplaceTextInFile(file string, text string, replacement string) error {
|
||||||
updated := strings.ReplaceAll(str, text, replacement)
|
updated := strings.ReplaceAll(str, text, replacement)
|
||||||
return os.WriteFile(file, []byte(updated), 0644)
|
return os.WriteFile(file, []byte(updated), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReplaceTextInDirRecursive(dir string, text string, replacement string, filter func(file string) bool) error {
|
||||||
|
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if filter(path) {
|
||||||
|
_ = ReplaceTextInFile(path, text, replacement)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func startWatcher(cb func(file []*fsnotify.Event)) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if event.Has(fsnotify.Write) {
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
|
||||||
events = append(events, &event)
|
events = append(events, &event)
|
||||||
go cb(events)
|
go cb(events)
|
||||||
events = make([]*fsnotify.Event, 0)
|
events = make([]*fsnotify.Event, 0)
|
||||||
|
|
|
||||||
2
framework/assets/dist/htmgo.js
vendored
2
framework/assets/dist/htmgo.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -10,6 +10,12 @@ type WsOpts = {
|
||||||
export function createWebSocketClient(opts: WsOpts) {
|
export function createWebSocketClient(opts: WsOpts) {
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
const connect = (tries: number) => {
|
const connect = (tries: number) => {
|
||||||
|
|
||||||
|
if(tries > 50) {
|
||||||
|
console.error('failed to connect to websocket after 50 tries, please reload the page');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket = new WebSocket(opts.url);
|
socket = new WebSocket(opts.url);
|
||||||
// Handle connection open
|
// Handle connection open
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,29 @@ package h
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/maddalax/htmgo/framework/htmgo/service"
|
||||||
"github.com/maddalax/htmgo/framework/util/process"
|
"github.com/maddalax/htmgo/framework/util/process"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type RequestContext struct {
|
||||||
|
echo.Context
|
||||||
|
locator *service.Locator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestContext) ServiceLocator() *service.Locator {
|
||||||
|
return c.locator
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppOpts struct {
|
||||||
LiveReload bool
|
LiveReload bool
|
||||||
|
ServiceLocator *service.Locator
|
||||||
|
Register func(echo *echo.Echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Opts AppOpts
|
||||||
Echo *echo.Echo
|
Echo *echo.Echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,16 +38,32 @@ func GetApp() *App {
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(app *echo.Echo, opts App) {
|
func Start(opts AppOpts) {
|
||||||
instance = &opts
|
e := echo.New()
|
||||||
instance.start(app)
|
instance := App{
|
||||||
|
Opts: opts,
|
||||||
|
Echo: e,
|
||||||
|
}
|
||||||
|
instance.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a App) start(app *echo.Echo) {
|
func (a App) start() {
|
||||||
|
|
||||||
a.Echo = app
|
a.Echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
cc := &RequestContext{
|
||||||
|
c,
|
||||||
|
a.Opts.ServiceLocator,
|
||||||
|
}
|
||||||
|
return next(cc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if a.LiveReload {
|
if a.Opts.Register != nil {
|
||||||
|
a.Opts.Register(a.Echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Opts.LiveReload {
|
||||||
AddLiveReloadHandler("/dev/livereload", a.Echo)
|
AddLiveReloadHandler("/dev/livereload", a.Echo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package h
|
package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"html"
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -51,10 +50,10 @@ func NewPartial(root Renderable) *Partial {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPartialPath(partial func(ctx echo.Context) *Partial) string {
|
func GetPartialPath(partial func(ctx *RequestContext) *Partial) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPartialPathWithQs(partial func(ctx echo.Context) *Partial, qs string) string {
|
func GetPartialPathWithQs(partial func(ctx *RequestContext) *Partial, qs string) string {
|
||||||
return html.EscapeString(GetPartialPath(partial) + "?" + qs)
|
return html.EscapeString(GetPartialPath(partial) + "?" + qs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package h
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"html"
|
"html"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -77,6 +76,10 @@ func Attributes(attrs map[string]string) Renderable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Checked() Renderable {
|
||||||
|
return Attribute("checked", "true")
|
||||||
|
}
|
||||||
|
|
||||||
func Boost() Renderable {
|
func Boost() Renderable {
|
||||||
return Attribute("hx-boost", "true")
|
return Attribute("hx-boost", "true")
|
||||||
}
|
}
|
||||||
|
|
@ -101,11 +104,11 @@ func Get(path string) Renderable {
|
||||||
return Attribute("hx-get", path)
|
return Attribute("hx-get", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPartial(partial func(ctx echo.Context) *Partial) Renderable {
|
func GetPartial(partial func(ctx *RequestContext) *Partial) Renderable {
|
||||||
return Get(GetPartialPath(partial))
|
return Get(GetPartialPath(partial))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPartialWithQs(partial func(ctx echo.Context) *Partial, qs string) Renderable {
|
func GetPartialWithQs(partial func(ctx *RequestContext) *Partial, qs string) Renderable {
|
||||||
return Get(GetPartialPathWithQs(partial, qs))
|
return Get(GetPartialPathWithQs(partial, qs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,13 +122,13 @@ type ReloadParams struct {
|
||||||
Children Renderable
|
Children Renderable
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewOnLoad(partial func(ctx echo.Context) *Partial) Renderable {
|
func ViewOnLoad(partial func(ctx *RequestContext) *Partial) Renderable {
|
||||||
return View(partial, ReloadParams{
|
return View(partial, ReloadParams{
|
||||||
Triggers: CreateTriggers("load"),
|
Triggers: CreateTriggers("load"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func View(partial func(ctx echo.Context) *Partial, params ReloadParams) Renderable {
|
func View(partial func(ctx *RequestContext) *Partial, params ReloadParams) Renderable {
|
||||||
return Div(Attributes(map[string]string{
|
return Div(Attributes(map[string]string{
|
||||||
"hx-get": GetPartialPath(partial),
|
"hx-get": GetPartialPath(partial),
|
||||||
"hx-trigger": strings.Join(params.Triggers, ", "),
|
"hx-trigger": strings.Join(params.Triggers, ", "),
|
||||||
|
|
@ -133,7 +136,7 @@ func View(partial func(ctx echo.Context) *Partial, params ReloadParams) Renderab
|
||||||
}), params.Children)
|
}), params.Children)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PartialWithTriggers(partial func(ctx echo.Context) *Partial, triggers ...string) Renderable {
|
func PartialWithTriggers(partial func(ctx *RequestContext) *Partial, triggers ...string) Renderable {
|
||||||
return Div(Attributes(map[string]string{
|
return Div(Attributes(map[string]string{
|
||||||
"hx-get": GetPartialPath(partial),
|
"hx-get": GetPartialPath(partial),
|
||||||
"hx-trigger": strings.Join(triggers, ", "),
|
"hx-trigger": strings.Join(triggers, ", "),
|
||||||
|
|
@ -283,7 +286,7 @@ func CombineHeaders(headers ...*Headers) *Headers {
|
||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
func CurrentPath(ctx echo.Context) string {
|
func CurrentPath(ctx *RequestContext) string {
|
||||||
current := ctx.Request().Header.Get("Hx-Current-Url")
|
current := ctx.Request().Header.Get("Hx-Current-Url")
|
||||||
parsed, err := url.Parse(current)
|
parsed, err := url.Parse(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -292,7 +295,7 @@ func CurrentPath(ctx echo.Context) string {
|
||||||
return parsed.Path
|
return parsed.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushQsHeader(ctx echo.Context, key string, value string) *Headers {
|
func PushQsHeader(ctx *RequestContext, key string, value string) *Headers {
|
||||||
current := ctx.Request().Header.Get("Hx-Current-Url")
|
current := ctx.Request().Header.Get("Hx-Current-Url")
|
||||||
parsed, err := url.Parse(current)
|
parsed, err := url.Parse(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -512,11 +515,11 @@ func IfElseLazy(condition bool, cb1 func() Renderable, cb2 func() Renderable) Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTriggerName(ctx echo.Context) string {
|
func GetTriggerName(ctx *RequestContext) string {
|
||||||
return ctx.Request().Header.Get("HX-Trigger-Name")
|
return ctx.Request().Header.Get("HX-Trigger-Name")
|
||||||
}
|
}
|
||||||
|
|
||||||
func IfHtmxRequest(ctx echo.Context, node Renderable) Renderable {
|
func IfHtmxRequest(ctx *RequestContext, node Renderable) Renderable {
|
||||||
if ctx.Get("HX-Request") != "" {
|
if ctx.Get("HX-Request") != "" {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
@ -535,11 +538,11 @@ func NewSwap(selector string, content *Node) SwapArg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Swap(ctx echo.Context, content Renderable) Renderable {
|
func Swap(ctx *RequestContext, content Renderable) Renderable {
|
||||||
return SwapWithSelector(ctx, "", content)
|
return SwapWithSelector(ctx, "", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SwapWithSelector(ctx echo.Context, selector string, content Renderable) Renderable {
|
func SwapWithSelector(ctx *RequestContext, selector string, content Renderable) Renderable {
|
||||||
if ctx == nil || ctx.Get("HX-Request") == "" {
|
if ctx == nil || ctx.Get("HX-Request") == "" {
|
||||||
return Empty()
|
return Empty()
|
||||||
}
|
}
|
||||||
|
|
@ -547,7 +550,7 @@ func SwapWithSelector(ctx echo.Context, selector string, content Renderable) Ren
|
||||||
return c.AppendChild(OutOfBandSwap(selector))
|
return c.AppendChild(OutOfBandSwap(selector))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SwapMany(ctx echo.Context, args ...SwapArg) Renderable {
|
func SwapMany(ctx *RequestContext, args ...SwapArg) Renderable {
|
||||||
if ctx.Get("HX-Request") == "" {
|
if ctx.Get("HX-Request") == "" {
|
||||||
return Empty()
|
return Empty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
framework/htmgo/service/locator.go
Normal file
77
framework/htmgo/service/locator.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Lifecycle = string
|
||||||
|
|
||||||
|
var (
|
||||||
|
Singleton Lifecycle = "singleton"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
cb func() any
|
||||||
|
lifecycle Lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
type Locator struct {
|
||||||
|
services map[string]Provider
|
||||||
|
cache map[string]any
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocator() *Locator {
|
||||||
|
return &Locator{
|
||||||
|
services: make(map[string]Provider),
|
||||||
|
cache: make(map[string]any),
|
||||||
|
mutex: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locator) setCache(key string, value any) {
|
||||||
|
l.cache[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locator) getCache(key string) any {
|
||||||
|
return l.cache[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get[T any](locator *Locator) *T {
|
||||||
|
locator.mutex.RLock()
|
||||||
|
i := new(T)
|
||||||
|
t := reflect.TypeOf(i).String()
|
||||||
|
|
||||||
|
cached := locator.getCache(t)
|
||||||
|
|
||||||
|
if cached != nil {
|
||||||
|
locator.mutex.RUnlock()
|
||||||
|
return cached.(*T)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := locator.services[t]
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("%s is not registered in the service locator", t)
|
||||||
|
}
|
||||||
|
cb := entry.cb().(*T)
|
||||||
|
locator.mutex.RUnlock()
|
||||||
|
locator.mutex.Lock()
|
||||||
|
if entry.lifecycle == Singleton {
|
||||||
|
locator.setCache(t, cb)
|
||||||
|
}
|
||||||
|
locator.mutex.Unlock()
|
||||||
|
return cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set[T any](locator *Locator, lifecycle Lifecycle, value func() *T) {
|
||||||
|
t := reflect.TypeOf(value)
|
||||||
|
rt := t.Out(0)
|
||||||
|
locator.services[rt.String()] = Provider{
|
||||||
|
cb: func() any {
|
||||||
|
return value()
|
||||||
|
},
|
||||||
|
lifecycle: lifecycle,
|
||||||
|
}
|
||||||
|
}
|
||||||
4
starter-template/.gitignore
vendored
Normal file
4
starter-template/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/assets/dist
|
||||||
|
tmp
|
||||||
|
node_modules
|
||||||
|
.idea
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
"github.com/maddalax/htmgo/framework/h"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"log"
|
"log"
|
||||||
"starter-template/ent"
|
|
||||||
"starter-template/pages"
|
"starter-template/pages"
|
||||||
"starter-template/partials/load"
|
"starter-template/partials/load"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -21,16 +19,6 @@ func main() {
|
||||||
load.RegisterPartials(f)
|
load.RegisterPartials(f)
|
||||||
pages.RegisterPages(f)
|
pages.RegisterPages(f)
|
||||||
|
|
||||||
client, err := ent.Open("sqlite3", "file:ent.db?cache=shared&_fk=1")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
// Run the auto migration tool.
|
|
||||||
if err := client.Schema.Create(context.Background()); err != nil {
|
|
||||||
log.Fatalf("failed schema resources: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("main() ready in %s", time.Since(startTime))
|
log.Printf("main() ready in %s", time.Since(startTime))
|
||||||
h.Start(f, h.App{
|
h.Start(f, h.App{
|
||||||
LiveReload: true,
|
LiveReload: true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue