From e81fce91c45194339c7e31833cd37a9c754ddc08 Mon Sep 17 00:00:00 2001 From: Sebastiano Bellinzis Date: Sun, 1 Dec 2024 20:44:09 +0100 Subject: [PATCH] refactor to work with connectors as mentioned in #8 --- INTEGRATIONS.md | 445 +++++++++++++++++++++++++++++++++++ README.md | 16 +- connector/alpine-ajax.go | 22 ++ connector/alpine.go | 22 ++ connector/connector.go | 87 +++++++ connector/htmx.go | 35 +++ connector/partial.go | 16 ++ connector/stimulus.go | 22 ++ connector/turbo.go | 16 ++ connector/unpoly.go | 22 ++ connector/vuejs.go | 22 ++ examples/tabs-htmx/main.go | 10 +- examples/tabs/content.gohtml | 6 +- examples/tabs/index.gohtml | 2 +- examples/tabs/main.go | 7 +- js/partial.js | 5 +- partial.go | 151 +++++++----- partial_test.go | 10 +- service.go | 46 ++-- template_functions.go | 3 +- 20 files changed, 848 insertions(+), 117 deletions(-) create mode 100644 INTEGRATIONS.md create mode 100644 connector/alpine-ajax.go create mode 100644 connector/alpine.go create mode 100644 connector/connector.go create mode 100644 connector/htmx.go create mode 100644 connector/partial.go create mode 100644 connector/stimulus.go create mode 100644 connector/turbo.go create mode 100644 connector/unpoly.go create mode 100644 connector/vuejs.go diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md new file mode 100644 index 0000000..5984ef1 --- /dev/null +++ b/INTEGRATIONS.md @@ -0,0 +1,445 @@ +# Supported Integrations (connectors) + +`go-partial` currently supports the following connectors: + +- HTMX +- Turbo +- Unpoly +- Alpine.js +- Stimulus +- Partial (Custom Connector) + +## HTMX + +### Description: +[HTMX](https://htmx.org/) allows you to use AJAX, WebSockets, and Server-Sent Events directly in HTML using attributes. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the HTMX connector +contentPartial.SetConnector(connector.NewHTMX(&connector.Config{ + UseURLQuery: true, // Enable fallback to URL query parameters +})) + +// Optionally add actions or selections +contentPartial.WithAction(func(ctx context.Context, p *partial.Partial, data *partial.Data) (*partial.Partial, error) { + // Action logic here + return p, nil +}) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html + + + + + +
+ +
+``` + +### alternative: +```html + + +``` + +### alternative 2: +```html + + +``` + +## Turbo +### Description: +[Turbo](https://turbo.hotwired.dev/) speeds up web applications by reducing the amount of custom JavaScript needed to provide rich, modern user experiences. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Turbo connector +contentPartial.SetConnector(connector.NewTurbo(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html + + + + + +Tab 1 +Tab 2 +``` + +## Unpoly +### Description: +[Unpoly](https://unpoly.com/) enables fast and flexible server-side rendering with minimal custom JavaScript. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Unpoly connector +contentPartial.SetConnector(connector.NewUnpoly(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html + +Tab 1 +Tab 2 + + +
+ +
+``` + +### Alternative: +```html +Tab 1 +Tab 2 +``` + +## Alpine.js +### Description: +[Alpine.js](https://alpinejs.dev/) offers a minimal and declarative way to render reactive components in the browser. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Alpine.js connector +contentPartial.SetConnector(connector.NewAlpine(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html +
+ +
+ +
+ +
+ +
+ + +
+ +
+
+``` + +## Alpine Ajax +### Description: +[Alpine Ajax](https://alpine-ajax.js.org) is an Alpine.js plugin that enables your HTML elements to request remote content from your server. +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Alpine-AJAX connector +contentPartial.SetConnector(connector.NewAlpineAjax(&connector.Config{ +UseURLQuery: true, // Enable fallback to URL query parameters +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html + + + + + +
+ + + + + + +
+ +
+
+ +``` + +## Stimulus +### Description: +[Stimulus](https://stimulus.hotwired.dev/) is a JavaScript framework that enhances static or server-rendered HTML with just enough behavior. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Stimulus connector +contentPartial.SetConnector(connector.NewStimulus(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html +
+ + + + + +
+ +
+
+ + +``` + +## Partial (Custom Connector) +### Description: +The Partial connector is a simple, custom connector provided by go-partial. It can be used when you don't rely on any specific front-end library. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the custom Partial connector +contentPartial.SetConnector(connector.NewPartial(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +``` +### Client-Side Usage: +```html + + + + +
+ +
+``` + +## Vue.js +### Description: +[Vue.js](https://vuejs.org/) is a progressive JavaScript framework for building user interfaces. + +### Note: +Integrating go-partial with Vue.js for partial HTML updates is possible but comes with limitations. For small sections of the page or simple content updates, it can work. For larger applications, consider whether server-rendered partials align with your architecture. + +### Server-Side Setup: +```go +import ( + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" +) + +// Create a new partial +contentPartial := partial.New("templates/content.gohtml").ID("content") + +// Set the Vue connector +contentPartial.SetConnector(connector.NewVue(&connector.Config{ + UseURLQuery: true, +})) + +// Handler function +func contentHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := contentPartial.WriteWithRequest(ctx, w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +### Client-Side Setup: +```html + + + +``` + +### using axios: +```javascript +methods: { + loadContent(select) { + axios.get('/content', { + headers: { + 'X-Vue-Target': 'content', + 'X-Vue-Select': select + } + }) + .then(response => { + this.content = response.data; + }); + } +} +``` \ No newline at end of file diff --git a/README.md b/README.md index 14c1e00..a021d93 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,20 @@ go get github.com/donseba/go-partial ## Advanced use cases Advanced usecases are documented in the [ADVANCED.md](ADVANCED.md) file -## Basic Usage +## Integrations +Several integrations are available, detailed information can be found in the [INTEGRATIONS.md](INTEGRATIONS.md) file +- htmx +- Turbo +- Stimulus +- Unpoly +- Alpine.js / Alpine Ajax (not great) +- Vue.js (not great) +- Standalone +## Basic Usage Here's a simple example of how to use the package to render a template. ### 1. Create a Service - The `Service` holds global configurations and data. ```go @@ -44,7 +52,6 @@ service.SetData(map[string]any{ ``` ## 2. Create a Layout - The `Layout` manages the overall structure of your templates. ```go layout := service.NewLayout() @@ -54,7 +61,6 @@ layout.SetData(map[string]any{ ``` ### 3. Define Partials - Create `Partial` instances for the content and any other components. ```go @@ -81,7 +87,6 @@ func handler(w http.ResponseWriter, r *http.Request) { ``` ## Template Files - templates/layout.html ```html @@ -103,7 +108,6 @@ Note: In the layout template, we use {{ child "content" }} to render the content ### Using Global and Layout Data - - **Global Data (ServiceData)**: Set on the Service, accessible via {{.Service}} in templates. - **Layout Data (LayoutData)**: Set on the Layout, accessible via {{.Layout}} in templates. - **Partial Data (Data)**: Set on individual Partial instances, accessible via {{.Data}} in templates. diff --git a/connector/alpine-ajax.go b/connector/alpine-ajax.go new file mode 100644 index 0000000..d6a8577 --- /dev/null +++ b/connector/alpine-ajax.go @@ -0,0 +1,22 @@ +package connector + +import "net/http" + +type AlpineAjax struct { + base +} + +func NewAlpineAjax(c *Config) Connector { + return &AlpineAjax{ + base: base{ + config: c, + targetHeader: "X-Alpine-Target", + selectHeader: "X-Alpine-Select", + actionHeader: "X-Alpine-Action", + }, + } +} + +func (a *AlpineAjax) RenderPartial(r *http.Request) bool { + return r.Header.Get(a.targetHeader) != "" +} diff --git a/connector/alpine.go b/connector/alpine.go new file mode 100644 index 0000000..624f321 --- /dev/null +++ b/connector/alpine.go @@ -0,0 +1,22 @@ +package connector + +import "net/http" + +type Alpine struct { + base +} + +func NewAlpine(c *Config) Connector { + return &Alpine{ + base: base{ + config: c, + targetHeader: "X-Alpine-Target", + selectHeader: "X-Alpine-Select", + actionHeader: "X-Alpine-Action", + }, + } +} + +func (a *Alpine) RenderPartial(r *http.Request) bool { + return r.Header.Get(a.targetHeader) != "" +} diff --git a/connector/connector.go b/connector/connector.go new file mode 100644 index 0000000..0b8c007 --- /dev/null +++ b/connector/connector.go @@ -0,0 +1,87 @@ +package connector + +import "net/http" + +type ( + Connector interface { + RenderPartial(r *http.Request) bool + GetTargetValue(r *http.Request) string + GetSelectValue(r *http.Request) string + GetActionValue(r *http.Request) string + + GetTargetHeader() string + GetSelectHeader() string + GetActionHeader() string + } + + Config struct { + UseURLQuery bool + } + + base struct { + config *Config + targetHeader string + selectHeader string + actionHeader string + } +) + +func (x *base) RenderPartial(r *http.Request) bool { + return r.Header.Get(x.targetHeader) != "" +} + +func (x *base) GetTargetHeader() string { + return x.targetHeader +} + +func (x *base) GetSelectHeader() string { + return x.selectHeader +} + +func (x *base) GetActionHeader() string { + return x.actionHeader +} + +func (x *base) GetTargetValue(r *http.Request) string { + if targetValue := r.Header.Get(x.targetHeader); targetValue != "" { + return targetValue + } + + if x.config.useURLQuery() { + return r.URL.Query().Get("target") + } + + return "" +} + +func (x *base) GetSelectValue(r *http.Request) string { + if selectValue := r.Header.Get(x.selectHeader); selectValue != "" { + return selectValue + } + + if x.config.useURLQuery() { + return r.URL.Query().Get("select") + } + + return "" +} + +func (x *base) GetActionValue(r *http.Request) string { + if actionValue := r.Header.Get(x.actionHeader); actionValue != "" { + return actionValue + } + + if x.config.useURLQuery() { + return r.URL.Query().Get("action") + } + + return "" +} + +func (c *Config) useURLQuery() bool { + if c == nil { + return false + } + + return c.UseURLQuery +} diff --git a/connector/htmx.go b/connector/htmx.go new file mode 100644 index 0000000..66838e1 --- /dev/null +++ b/connector/htmx.go @@ -0,0 +1,35 @@ +package connector + +import ( + "net/http" +) + +type HTMX struct { + base + + requestHeader string + boostedHeader string + historyRestoreRequestHeader string +} + +func NewHTMX(c *Config) Connector { + return &HTMX{ + base: base{ + config: c, + targetHeader: "HX-Target", + selectHeader: "X-Select", + actionHeader: "X-Action", + }, + requestHeader: "HX-Request", + boostedHeader: "HX-Boosted", + historyRestoreRequestHeader: "HX-History-Restore-Request", + } +} + +func (h *HTMX) RenderPartial(r *http.Request) bool { + hxRequest := r.Header.Get(h.requestHeader) + hxBoosted := r.Header.Get(h.boostedHeader) + hxHistoryRestoreRequest := r.Header.Get(h.historyRestoreRequestHeader) + + return (hxRequest == "true" || hxBoosted == "true") && hxHistoryRestoreRequest != "true" +} diff --git a/connector/partial.go b/connector/partial.go new file mode 100644 index 0000000..e9874a1 --- /dev/null +++ b/connector/partial.go @@ -0,0 +1,16 @@ +package connector + +type Partial struct { + base +} + +func NewPartial(c *Config) Connector { + return &Partial{ + base: base{ + config: c, + targetHeader: "X-Target", + selectHeader: "X-Select", + actionHeader: "X-Action", + }, + } +} diff --git a/connector/stimulus.go b/connector/stimulus.go new file mode 100644 index 0000000..3e9851d --- /dev/null +++ b/connector/stimulus.go @@ -0,0 +1,22 @@ +package connector + +import "net/http" + +type Stimulus struct { + base +} + +func NewStimulus(c *Config) Connector { + return &Stimulus{ + base: base{ + config: c, + targetHeader: "X-Stimulus-Target", + selectHeader: "X-Stimulus-Select", + actionHeader: "X-Stimulus-Action", + }, + } +} + +func (s *Stimulus) RenderPartial(r *http.Request) bool { + return r.Header.Get(s.targetHeader) != "" +} diff --git a/connector/turbo.go b/connector/turbo.go new file mode 100644 index 0000000..f17c999 --- /dev/null +++ b/connector/turbo.go @@ -0,0 +1,16 @@ +package connector + +type Turbo struct { + base +} + +func NewTurbo(c *Config) Connector { + return &Turbo{ + base: base{ + config: c, + targetHeader: "Turbo-Frame", + selectHeader: "Turbo-Select", + actionHeader: "Turbo-Action", + }, + } +} diff --git a/connector/unpoly.go b/connector/unpoly.go new file mode 100644 index 0000000..0d7e254 --- /dev/null +++ b/connector/unpoly.go @@ -0,0 +1,22 @@ +package connector + +import "net/http" + +type Unpoly struct { + base +} + +func NewUnpoly(c *Config) Connector { + return &Unpoly{ + base: base{ + config: c, + targetHeader: "X-Up-Target", + selectHeader: "X-Up-Select", + actionHeader: "X-Up-Action", + }, + } +} + +func (u *Unpoly) RenderPartial(r *http.Request) bool { + return r.Header.Get(u.targetHeader) != "" +} diff --git a/connector/vuejs.go b/connector/vuejs.go new file mode 100644 index 0000000..fff0e78 --- /dev/null +++ b/connector/vuejs.go @@ -0,0 +1,22 @@ +package connector + +import "net/http" + +type Vue struct { + base +} + +func NewVue(c *Config) Connector { + return &Vue{ + base: base{ + config: c, + targetHeader: "X-Vue-Target", + selectHeader: "X-Vue-Select", + actionHeader: "X-Vue-Action", + }, + } +} + +func (v *Vue) RenderPartial(r *http.Request) bool { + return r.Header.Get(v.targetHeader) != "" +} diff --git a/examples/tabs-htmx/main.go b/examples/tabs-htmx/main.go index 59e9dc4..cd4622a 100644 --- a/examples/tabs-htmx/main.go +++ b/examples/tabs-htmx/main.go @@ -2,9 +2,11 @@ package main import ( "fmt" - "github.com/donseba/go-partial" "log/slog" "net/http" + + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" ) type ( @@ -18,14 +20,14 @@ func main() { app := &App{ PartialService: partial.NewService(&partial.Config{ - PartialHeader: "HX-Target", - Logger: logger, + Logger: logger, + Connector: connector.NewHTMX(nil), }), } mux := http.NewServeMux() - mux.Handle("GET /files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./files")))) + mux.Handle("GET /js/", http.StripPrefix("/js/", http.FileServer(http.Dir("../../js")))) mux.HandleFunc("GET /", app.home) diff --git a/examples/tabs/content.gohtml b/examples/tabs/content.gohtml index 56d298a..eaf3473 100644 --- a/examples/tabs/content.gohtml +++ b/examples/tabs/content.gohtml @@ -2,13 +2,13 @@ diff --git a/examples/tabs/index.gohtml b/examples/tabs/index.gohtml index e601f2c..18c3dbc 100644 --- a/examples/tabs/index.gohtml +++ b/examples/tabs/index.gohtml @@ -3,7 +3,7 @@ Tab Example - + diff --git a/examples/tabs/main.go b/examples/tabs/main.go index 263019f..e830223 100644 --- a/examples/tabs/main.go +++ b/examples/tabs/main.go @@ -2,10 +2,12 @@ package main import ( "fmt" - "github.com/donseba/go-partial" "log/slog" "net/http" "path/filepath" + + "github.com/donseba/go-partial" + "github.com/donseba/go-partial/connector" ) type ( @@ -20,6 +22,9 @@ func main() { app := &App{ PartialService: partial.NewService(&partial.Config{ Logger: logger, + Connector: connector.NewPartial(&connector.Config{ + UseURLQuery: true, + }), }), } diff --git a/js/partial.js b/js/partial.js index 7dab576..21396f8 100644 --- a/js/partial.js +++ b/js/partial.js @@ -58,9 +58,9 @@ class Partial { }; this.SERIALIZE_TYPES = { - JSON: 'json', + JSON: 'json', NESTED_JSON: 'nested-json', - XML: 'xml', + XML: 'xml', }; // Store options with default values @@ -557,7 +557,6 @@ class Partial { } // Merge paramsObject with bodyData - console.log(paramsObject) if (paramsObject && Object.keys(paramsObject).length > 0) { if (bodyData instanceof FormData) { // Append params to FormData diff --git a/partial.go b/partial.go index 77f2054..10f9342 100644 --- a/partial.go +++ b/partial.go @@ -14,6 +14,8 @@ import ( "reflect" "strings" "sync" + + "github.com/donseba/go-partial/connector" ) var ( @@ -51,12 +53,7 @@ type ( swapOOB bool fs fs.FS logger Logger - partialHeader string - selectHeader string - actionHeader string - requestedPartial string - requestedAction string - requestedSelect string + connector connector.Connector useCache bool templates []string combinedFunctions template.FuncMap @@ -150,6 +147,12 @@ func (p *Partial) AddData(key string, value any) *Partial { return p } +// SetConnector sets the connector for the partial. +func (p *Partial) SetConnector(connector connector.Connector) *Partial { + p.connector = connector + return p +} + // MergeData merges the data into the partial. func (p *Partial) MergeData(data map[string]any, override bool) *Partial { for k, v := range data { @@ -286,11 +289,15 @@ func (p *Partial) RenderWithRequest(ctx context.Context, r *http.Request) (templ } p.request = r - p.requestedPartial = r.Header.Get(p.getPartialHeader()) - p.requestedAction = r.Header.Get(p.getActionHeader()) - p.requestedSelect = r.Header.Get(p.getSelectHeader()) + if p.connector == nil { + p.connector = connector.NewPartial(nil) + } - return p.renderWithTarget(ctx, r) + if p.connector.RenderPartial(r) { + return p.renderWithTarget(ctx, r) + } + + return p.renderSelf(ctx, r) } // WriteWithRequest writes the partial to the http.ResponseWriter. @@ -367,16 +374,17 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap { } funcs["partialHeader"] = func() string { - return p.getPartialHeader() + return p.getConnector().GetTargetHeader() } funcs["requestedPartial"] = func() string { - return p.getRequestedPartial() + return p.getConnector().GetTargetValue(p.getRequest()) } funcs["ifRequestedPartial"] = func(out any, in ...string) any { + target := p.getConnector().GetTargetValue(p.getRequest()) for _, v := range in { - if v == p.getRequestedPartial() { + if v == target { return out } } @@ -384,19 +392,22 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap { } funcs["selectHeader"] = func() string { - return p.getSelectHeader() + return p.getConnector().GetSelectHeader() } funcs["requestedSelect"] = func() string { - if p.getRequestedSelect() == "" { + requestedSelect := p.getConnector().GetSelectValue(p.getRequest()) + + if requestedSelect == "" { return p.selection.Default } - return p.getRequestedSelect() + return requestedSelect } funcs["ifRequestedSelect"] = func(out any, in ...string) any { + selected := p.getConnector().GetSelectValue(p.getRequest()) for _, v := range in { - if v == p.getRequestedSelect() { + if v == selected { return out } } @@ -404,16 +415,17 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap { } funcs["actionHeader"] = func() string { - return p.getActionHeader() + return p.getConnector().GetActionHeader() } funcs["requestedAction"] = func() string { - return p.GetRequestedAction() + return p.getConnector().GetActionValue(p.getRequest()) } funcs["ifRequestedAction"] = func(out any, in ...string) any { + action := p.getConnector().GetActionValue(p.getRequest()) for _, v := range in { - if v == p.GetRequestedAction() { + if v == action { return out } } @@ -457,24 +469,45 @@ func (p *Partial) getLayoutData() map[string]any { return p.layoutData } -func (p *Partial) getPartialHeader() string { - if p.partialHeader != "" { - return p.partialHeader +// +//func (p *Partial) getPartialHeader() string { +// if p.partialHeader != "" { +// return p.partialHeader +// } +// if p.parent != nil { +// return p.parent.getPartialHeader() +// } +// return defaultTargetHeader +//} +// +//func (p *Partial) getSelectHeader() string { +// if p.selectHeader != "" { +// return p.selectHeader +// } +// if p.parent != nil { +// return p.parent.getSelectHeader() +// } +// return defaultSelectHeader +//} +// +//func (p *Partial) getActionHeader() string { +// if p.actionHeader != "" { +// return p.actionHeader +// } +// if p.parent != nil { +// return p.parent.getActionHeader() +// } +// return defaultActionHeader +//} + +func (p *Partial) getConnector() connector.Connector { + if p.connector != nil { + return p.connector } if p.parent != nil { - return p.parent.getPartialHeader() + return p.parent.getConnector() } - return defaultTargetHeader -} - -func (p *Partial) getSelectHeader() string { - if p.selectHeader != "" { - return p.selectHeader - } - if p.parent != nil { - return p.parent.getSelectHeader() - } - return defaultSelectHeader + return nil } func (p *Partial) getSelectionPartials() map[string]*Partial { @@ -484,16 +517,6 @@ func (p *Partial) getSelectionPartials() map[string]*Partial { return nil } -func (p *Partial) getActionHeader() string { - if p.actionHeader != "" { - return p.actionHeader - } - if p.parent != nil { - return p.parent.getActionHeader() - } - return defaultActionHeader -} - func (p *Partial) getRequest() *http.Request { if p.request != nil { return p.request @@ -533,19 +556,21 @@ func (p *Partial) getLogger() Logger { return p.logger } -func (p *Partial) getRequestedPartial() string { - if p.requestedPartial != "" { - return p.requestedPartial +func (p *Partial) GetRequestedPartial() string { + th := p.getConnector().GetTargetValue(p.getRequest()) + if th != "" { + return th } if p.parent != nil { - return p.parent.getRequestedPartial() + return p.parent.GetRequestedPartial() } return "" } func (p *Partial) GetRequestedAction() string { - if p.requestedAction != "" { - return p.requestedAction + ah := p.getConnector().GetActionValue(p.getRequest()) + if ah != "" { + return ah } if p.parent != nil { return p.parent.GetRequestedAction() @@ -553,18 +578,20 @@ func (p *Partial) GetRequestedAction() string { return "" } -func (p *Partial) getRequestedSelect() string { - if p.requestedSelect != "" { - return p.requestedSelect +func (p *Partial) GetRequestedSelect() string { + as := p.getConnector().GetSelectValue(p.getRequest()) + if as != "" { + return as } if p.parent != nil { - return p.parent.getRequestedSelect() + return p.parent.GetRequestedSelect() } return "" } func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request) (template.HTML, error) { - if p.getRequestedPartial() == "" || p.getRequestedPartial() == p.id { + requestedTarget := p.getConnector().GetTargetValue(p.getRequest()) + if requestedTarget == "" || requestedTarget == p.id { out, err := p.renderSelf(ctx, r) if err != nil { return "", err @@ -581,10 +608,10 @@ func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request) (templa } return out, nil } else { - c := p.recursiveChildLookup(p.getRequestedPartial(), make(map[string]bool)) + c := p.recursiveChildLookup(requestedTarget, make(map[string]bool)) if c == nil { - p.getLogger().Error("requested partial not found in parent", "id", p.getRequestedPartial(), "parent", p.id) - return "", fmt.Errorf("requested partial %s not found in parent %s", p.getRequestedPartial(), p.id) + p.getLogger().Error("requested partial not found in parent", "id", requestedTarget, "parent", p.id) + return "", fmt.Errorf("requested partial %s not found in parent %s", requestedTarget, p.id) } return c.renderWithTarget(ctx, r) } @@ -665,7 +692,6 @@ func (p *Partial) renderSelf(ctx context.Context, r *http.Request) (template.HTM p.getLogger().Error("error in action function", "error", err) return "", fmt.Errorf("error in action function: %w", err) } - //return actionPartial.renderSelf(ctx, r) } functions := p.getFuncs(data) @@ -757,10 +783,7 @@ func (p *Partial) clone() *Partial { swapOOB: p.swapOOB, fs: p.fs, logger: p.logger, - partialHeader: p.partialHeader, - selectHeader: p.selectHeader, - actionHeader: p.actionHeader, - requestedPartial: p.requestedPartial, + connector: p.connector, useCache: p.useCache, selection: p.selection, templates: append([]string{}, p.templates...), // Copy the slice diff --git a/partial_test.go b/partial_test.go index 642a7b7..4cf4c2f 100644 --- a/partial_test.go +++ b/partial_test.go @@ -8,6 +8,8 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/donseba/go-partial/connector" ) func TestNewRoot(t *testing.T) { @@ -539,8 +541,8 @@ func BenchmarkWithSelectMap(b *testing.B) { } service := NewService(&Config{ - PartialHeader: "X-Target", - UseCache: false, + Connector: connector.NewPartial(nil), + UseCache: false, }) layout := service.NewLayout().FS(fsys) @@ -572,8 +574,8 @@ func BenchmarkWithSelectMap(b *testing.B) { func BenchmarkRenderWithRequest(b *testing.B) { // Setup configuration and service cfg := &Config{ - PartialHeader: "X-Target", - UseCache: false, + Connector: connector.NewPartial(nil), + UseCache: false, } service := NewService(cfg) diff --git a/service.go b/service.go index 1005764..63d8491 100644 --- a/service.go +++ b/service.go @@ -7,6 +7,8 @@ import ( "log/slog" "net/http" "sync" + + "github.com/donseba/go-partial/connector" ) var ( @@ -25,19 +27,18 @@ type ( } Config struct { - PartialHeader string - SelectHeader string - ActionHeader string - UseCache bool - FuncMap template.FuncMap - Logger Logger - fs fs.FS + Connector connector.Connector + UseCache bool + FuncMap template.FuncMap + Logger Logger + fs fs.FS } Service struct { config *Config data map[string]any combinedFunctions template.FuncMap + connector connector.Connector funcMapLock sync.RWMutex // Add a read-write mutex } @@ -47,11 +48,9 @@ type ( content *Partial wrapper *Partial data map[string]any - requestedPartial string - requestedAction string - requestedSelect string request *http.Request combinedFunctions template.FuncMap + connector connector.Connector funcMapLock sync.RWMutex // Add a read-write mutex } ) @@ -62,18 +61,6 @@ func NewService(cfg *Config) *Service { cfg.FuncMap = DefaultTemplateFuncMap } - if cfg.PartialHeader == "" { - cfg.PartialHeader = defaultTargetHeader - } - - if cfg.SelectHeader == "" { - cfg.SelectHeader = defaultSelectHeader - } - - if cfg.ActionHeader == "" { - cfg.ActionHeader = defaultActionHeader - } - if cfg.Logger == nil { cfg.Logger = slog.Default().WithGroup("partial") } @@ -83,6 +70,7 @@ func NewService(cfg *Config) *Service { data: make(map[string]any), funcMapLock: sync.RWMutex{}, combinedFunctions: cfg.FuncMap, + connector: cfg.Connector, } } @@ -92,6 +80,7 @@ func (svc *Service) NewLayout() *Layout { service: svc, data: make(map[string]any), filesystem: svc.config.fs, + connector: svc.connector, combinedFunctions: svc.getFuncMap(), } } @@ -108,6 +97,11 @@ func (svc *Service) AddData(key string, value any) *Service { return svc } +func (svc *Service) SetConnector(conn connector.Connector) *Service { + svc.connector = conn + return svc +} + // MergeFuncMap merges the given FuncMap with the existing FuncMap. func (svc *Service) MergeFuncMap(funcMap template.FuncMap) { svc.funcMapLock.Lock() @@ -185,9 +179,6 @@ func (l *Layout) getFuncMap() template.FuncMap { // RenderWithRequest renders the partial with the given http.Request. func (l *Layout) RenderWithRequest(ctx context.Context, r *http.Request) (template.HTML, error) { - l.requestedPartial = r.Header.Get(l.service.config.PartialHeader) - l.requestedAction = r.Header.Get(l.service.config.ActionHeader) - l.requestedSelect = r.Header.Get(l.service.config.SelectHeader) l.request = r if l.wrapper != nil { @@ -231,14 +222,11 @@ func (l *Layout) applyConfigToPartial(p *Partial) { p.mergeFuncMapInternal(combinedFunctions) + p.connector = l.service.connector p.fs = l.filesystem p.logger = l.service.config.Logger p.useCache = l.service.config.UseCache p.globalData = l.service.data p.layoutData = l.data p.request = l.request - p.partialHeader = l.service.config.PartialHeader - p.selectHeader = l.service.config.SelectHeader - p.actionHeader = l.service.config.ActionHeader - p.requestedPartial = l.requestedPartial } diff --git a/template_functions.go b/template_functions.go index e281ca5..86b8375 100644 --- a/template_functions.go +++ b/template_functions.go @@ -160,7 +160,7 @@ func selectionFunc(p *Partial, data *Data) func() template.HTML { return template.HTML(fmt.Sprintf("no selection partials found in parent '%s'", p.id)) } - requestedSelect := p.getRequestedSelect() + requestedSelect := p.getConnector().GetSelectValue(p.getRequest()) if requestedSelect != "" { selectedPartial = partials[requestedSelect] } else { @@ -173,7 +173,6 @@ func selectionFunc(p *Partial, data *Data) func() template.HTML { } selectedPartial.fs = p.fs - //selectedPartial.parent = p html, err := selectedPartial.renderSelf(data.Ctx, p.getRequest()) if err != nil {