diff --git a/ADVANCED.md b/ADVANCED.md
new file mode 100644
index 0000000..6a5a7dd
--- /dev/null
+++ b/ADVANCED.md
@@ -0,0 +1,187 @@
+# Advanced Use Cases
+The go-partial package offers advanced features to handle dynamic content rendering based on user interactions or server-side logic. Below are three advanced use cases:
+
+- **WithSelection**: Selecting partials based on a selection key.
+- **WithAction**: Executing server-side actions during request processing.
+- **WithTemplateAction**: Invoking actions from within templates.
+
+## WithSelection
+### Purpose
+WithSelection allows you to select and render one of several predefined partials based on a selection key, such as a header value or query parameter. This is useful for rendering different content based on user interaction, like tabbed interfaces.
+### When to Use
+Use WithSelection when you have a static set of partials and need to render one of them based on a simple key provided by the client.
+### How to Use
+#### Step 1: Define the Partials
+Create partials for each selectable content.
+```go
+tab1Partial := partial.New("tab1.gohtml").ID("tab1")
+tab2Partial := partial.New("tab2.gohtml").ID("tab2")
+defaultPartial := partial.New("default.gohtml").ID("default")
+```
+
+#### Step 2: Create a Selection Map
+Map selection keys to their corresponding partials.
+```go
+partialsMap := map[string]*partial.Partial{
+"tab1": tab1Partial,
+"tab2": tab2Partial,
+"default": defaultPartial,
+}
+```
+
+#### Step 3: Set Up the Content Partial with Selection
+Use WithSelection to associate the selection map with your content partial.
+```go
+contentPartial := partial.New("content.gohtml").ID("content").WithSelection("default", partialsMap)
+```
+
+#### Step 4: Update the Template
+In your content.gohtml template, use the {{selection}} function to render the selected partial.
+```html
+
+ {{selection}}
+
+```
+
+#### Step 5: Handle the Request
+In your handler, render the partial as usual.
+```go
+func yourHandler(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
+Set the selection key via a header (e.g., X-Select) or another method.
+```html
+
+
+
+```
+
+## WithAction
+### Purpose
+WithAction allows you to execute server-side logic during request processing, such as handling form submissions or performing business logic, and then render a partial based on the result.
+### When to Use
+Use WithAction when you need to perform dynamic operations before rendering, such as processing form data, updating a database, or any logic that isn't just selecting a predefined partial.
+
+### How to Use
+#### Step 1: Define the Partial with an Action
+Attach an action function to the partial using WithAction.
+```go
+formPartial := partial.New("form.gohtml").ID("contactForm").WithAction(func(ctx context.Context, data *partial.Data) (*partial.Partial, error) {
+// Access form values
+name := data.Request.FormValue("name")
+email := data.Request.FormValue("email")
+
+ // Perform validation and business logic
+ if name == "" || email == "" {
+ errorPartial := partial.New("error.gohtml").AddData("Message", "Name and email are required.")
+ return errorPartial, nil
+ }
+
+ // Simulate saving to a database or performing an action
+ // ...
+
+ // Decide which partial to render next
+ successPartial := partial.New("success.gohtml").AddData("Name", name)
+ return successPartial, nil
+})
+```
+#### Step 2: Handle the Request
+In your handler, render the partial with the request.
+```go
+func submitHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ err := formPartial.WriteWithRequest(ctx, w, r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+```
+#### Step 3: Client-Side Usage
+Submit the form with an X-Action header specifying the partial ID.
+```html
+
+```
+#### Explanation
+
+- The form submission triggers the server to execute the action associated with contactForm.
+- The action processes the form data and returns a partial to render.
+- The server responds with the rendered partial (e.g., a success message).
+
+## WithTemplateAction
+### Purpose
+WithTemplateAction allows actions to be executed from within the template using a function like {{action "actionName"}}. This provides flexibility to execute actions conditionally or at specific points in your template.
+### When to Use
+Use WithTemplateAction when you need to invoke actions directly from the template, possibly under certain conditions, or when you have multiple actions within a single template.
+### How to Use
+
+#### Step 1: Define the Partial with Template Actions
+Attach template actions to your partial.
+```go
+myPartial := partial.New("mytemplate.gohtml").ID("myPartial").WithTemplateAction("loadDynamicContent", func(ctx context.Context, data *partial.Data) (*partial.Partial, error) {
+ // Load dynamic content
+ dynamicPartial := partial.New("dynamic.gohtml")
+ // Add data or perform operations
+ return dynamicPartial, nil
+})
+```
+#### Step 2: Update the Template
+In your mytemplate.gohtml template, invoke the action using the action function.
+```html
+
+
+ {{action "loadDynamicContent"}}
+
+
+```
+#### Step 3: Handle the Request
+Render the partial as usual.
+```go
+func yourHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ err := myPartial.WriteWithRequest(ctx, w, r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+```
+#### Use Cases
+
+- Conditional Execution
+ ```gotemplate
+ {{if .Data.ShowSpecialContent}}
+ {{action "loadSpecialContent"}}
+ {{end}}
+ ```
+- Lazy Loading
+ ```gotemplate
+
+ {{action "loadHeavyContent"}}
+
+ ```
+#### Explanation
+
+- Actions are only executed when the {{action "actionName"}} function is called in the template.
+- This allows for conditional or multiple action executions within the same template.
+- The server-side action returns a partial to render, which is then included at that point in the template.
+
+## Choosing the Right Approach
+- Use `WithSelection` when you have a set of predefined partials and want to select one based on a simple key.
+- Use `WithAction` when you need to perform server-side logic during request processing and render a partial based on the result.
+- Use `WithTemplateAction` when you want to invoke actions directly from within the template, especially for conditional execution or multiple actions.
+
+## Notes
+
+- **Separation of Concerns**: While WithTemplateAction provides flexibility, be cautious not to overload templates with business logic. Keep templates focused on presentation as much as possible.
+- **Error Handling**: Ensure that your actions handle errors gracefully and that your templates can render appropriately even if an action fails.
+- **Thread Safety**: If your application is concurrent, ensure that shared data is properly synchronized.
\ No newline at end of file
diff --git a/README.md b/README.md
index a33e286..1734adb 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@ To install the package, run:
go get github.com/donseba/go-partial
```
+## Advanced use cases
+Advanced usecases are documented in the [ADVANCED.md](ADVANCED.md) file
+
## Basic Usage
Here's a simple example of how to use the package to render a template.
diff --git a/js/htmx.partial.js b/js/htmx.partial.js
index 6a16283..2abd5ad 100644
--- a/js/htmx.partial.js
+++ b/js/htmx.partial.js
@@ -1,9 +1,19 @@
(function() {
htmx.on('htmx:configRequest', function(event) {
- var element = event.detail.elt;
- var partialValue = element.getAttribute('hx-partial');
+ let element = event.detail.elt;
+ let partialValue = element.getAttribute('hx-partial');
if (partialValue !== null) {
event.detail.headers['X-Partial'] = partialValue;
}
+
+ let selectValue = element.getAttribute('hx-select');
+ if (selectValue !== null) {
+ event.detail.headers['X-Select'] = selectValue;
+ }
+
+ let actionValue = element.getAttribute('hx-action');
+ if (actionValue !== null) {
+ event.detail.headers['X-Action'] = actionValue;
+ }
});
})();
\ No newline at end of file
diff --git a/partial.go b/partial.go
index c3729b1..f23a659 100644
--- a/partial.go
+++ b/partial.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"io/fs"
+ "log/slog"
"net/http"
"net/url"
"path"
@@ -22,12 +23,18 @@ var (
mutexCache = sync.Map{}
// protectedFunctionNames is a set of function names that are protected from being overridden
protectedFunctionNames = map[string]struct{}{
- "child": {},
- "context": {},
- "partialHeader": {},
- "requestHeader": {},
- "swapOOB": {},
- "url": {},
+ "action": {},
+ "actionHeader": {},
+ "child": {},
+ "context": {},
+ "partialHeader": {},
+ "requestedPartial": {},
+ "requestedAction": {},
+ "requestedSelect": {},
+ "selectHeader": {},
+ "selection": {},
+ "swapOOB": {},
+ "url": {},
}
)
@@ -36,11 +43,16 @@ type (
Partial struct {
id string
parent *Partial
+ request *http.Request
swapOOB bool
fs fs.FS
logger Logger
partialHeader string
- requestHeader string
+ selectHeader string
+ actionHeader string
+ requestedPartial string
+ requestedAction string
+ requestedSelect string
useCache bool
templates []string
combinedFunctions template.FuncMap
@@ -50,6 +62,14 @@ type (
mu sync.RWMutex
children map[string]*Partial
oobChildren map[string]struct{}
+ selection *Selection
+ templateAction func(ctx context.Context, data *Data) (*Partial, error)
+ action func(ctx context.Context, data *Data) (*Partial, error)
+ }
+
+ Selection struct {
+ Partials map[string]*Partial
+ Default string
}
// Data represents the data available to the partial.
@@ -139,9 +159,7 @@ func (p *Partial) MergeData(data map[string]any, override bool) *Partial {
// AddFunc adds a function to the partial.
func (p *Partial) AddFunc(name string, fn interface{}) *Partial {
if _, ok := protectedFunctionNames[name]; ok {
- if p.logger != nil {
- p.logger.Warn("function name is protected and cannot be overwritten", "function", name)
- }
+ p.getLogger().Warn("function name is protected and cannot be overwritten", "function", name)
return p
}
@@ -159,9 +177,7 @@ func (p *Partial) MergeFuncMap(funcMap template.FuncMap) {
for k, v := range funcMap {
if _, ok := protectedFunctionNames[k]; ok {
- if p.logger != nil {
- p.logger.Warn("function name is protected and cannot be overwritten", "function", k)
- }
+ p.getLogger().Warn("function name is protected and cannot be overwritten", "function", k)
continue
}
@@ -217,6 +233,30 @@ func (p *Partial) With(child *Partial) *Partial {
return p
}
+// WithAction adds callback action to the partial, which can do some logic and return a partial to render.
+func (p *Partial) WithAction(action func(ctx context.Context, data *Data) (*Partial, error)) *Partial {
+ p.action = action
+ return p
+}
+
+func (p *Partial) WithTemplateAction(templateAction func(ctx context.Context, data *Data) (*Partial, error)) *Partial {
+ p.templateAction = templateAction
+ return p
+}
+
+// WithSelectMap adds a selection partial to the partial.
+func (p *Partial) WithSelectMap(defaultKey string, partialsMap map[string]*Partial) *Partial {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ p.selection = &Selection{
+ Default: defaultKey,
+ Partials: partialsMap,
+ }
+
+ return p
+}
+
// SetParent sets the parent of the partial.
func (p *Partial) SetParent(parent *Partial) *Partial {
p.parent = parent
@@ -239,9 +279,12 @@ func (p *Partial) RenderWithRequest(ctx context.Context, r *http.Request) (templ
return "", errors.New("partial is not initialized")
}
- renderTarget := r.Header.Get(p.getPartialHeader())
+ p.request = r
+ p.requestedPartial = r.Header.Get(p.getPartialHeader())
+ p.requestedAction = r.Header.Get(p.getActionHeader())
+ p.requestedSelect = r.Header.Get(p.getSelectHeader())
- return p.renderWithTarget(ctx, r, renderTarget)
+ return p.renderWithTarget(ctx, r)
}
// WriteWithRequest writes the partial to the http.ResponseWriter.
@@ -253,17 +296,13 @@ func (p *Partial) WriteWithRequest(ctx context.Context, w http.ResponseWriter, r
out, err := p.RenderWithRequest(ctx, r)
if err != nil {
- if p.logger != nil {
- p.logger.Error("error rendering partial", "error", err)
- }
+ p.getLogger().Error("error rendering partial", "error", err)
return err
}
_, err = w.Write([]byte(out))
if err != nil {
- if p.logger != nil {
- p.logger.Error("error writing partial to response", "error", err)
- }
+ p.getLogger().Error("error writing partial to response", "error", err)
return err
}
@@ -313,37 +352,9 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap {
return p.swapOOB
}
- funcs["child"] = func(id string, vals ...any) template.HTML {
- if len(vals) > 0 && len(vals)%2 != 0 {
- if p.logger != nil {
- p.logger.Warn("invalid child data for partial, they come in key-value pairs", "id", id)
- }
- return template.HTML(fmt.Sprintf("invalid child data for partial '%s'", id))
- }
-
- d := make(map[string]any)
- for i := 0; i < len(vals); i += 2 {
- key, ok := vals[i].(string)
- if !ok {
- if p.logger != nil {
- p.logger.Warn("invalid child data key for partial, it must be a string", "id", id, "key", vals[i])
- }
- return template.HTML(fmt.Sprintf("invalid child data key for partial '%s', want string, got %T", id, vals[i]))
- }
- d[key] = vals[i+1]
- }
-
- html, err := p.renderChildPartial(data.Ctx, id, d)
- if err != nil {
- if p.logger != nil {
- p.logger.Error("error rendering partial", "id", id, "error", err)
- }
- // Handle error: you can log it and return an empty string or an error message
- return template.HTML(fmt.Sprintf("error rendering partial '%s': %v", id, err))
- }
-
- return html
- }
+ funcs["child"] = childFunc(p, data)
+ funcs["selection"] = selectionFunc(p, data)
+ funcs["action"] = actionFunc(p, data)
funcs["url"] = func() *url.URL {
return data.URL
@@ -353,14 +364,30 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap {
return data.Ctx
}
- funcs["requestHeader"] = func() string {
- return p.getRequestHeader()
- }
-
funcs["partialHeader"] = func() string {
return p.getPartialHeader()
}
+ funcs["requestedPartial"] = func() string {
+ return p.getRequestedPartial()
+ }
+
+ funcs["selectHeader"] = func() string {
+ return p.getSelectHeader()
+ }
+
+ funcs["requestedSelect"] = func() string {
+ return p.getRequestedSelect()
+ }
+
+ funcs["actionHeader"] = func() string {
+ return p.getActionHeader()
+ }
+
+ funcs["requestedAction"] = func() string {
+ return p.getRequestedAction()
+ }
+
return funcs
}
@@ -393,17 +420,44 @@ func (p *Partial) getPartialHeader() string {
if p.parent != nil {
return p.parent.getPartialHeader()
}
- return ""
+ return defaultPartialHeader
}
-func (p *Partial) getRequestHeader() string {
- if p.requestHeader != "" {
- return p.requestHeader
+func (p *Partial) getSelectHeader() string {
+ if p.selectHeader != "" {
+ return p.selectHeader
}
if p.parent != nil {
- return p.parent.getRequestHeader()
+ return p.parent.getSelectHeader()
}
- return ""
+ return defaultSelectHeader
+}
+
+func (p *Partial) getSelectionPartials() map[string]*Partial {
+ if p.selection != nil {
+ return p.selection.Partials
+ }
+ 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
+ }
+ if p.parent != nil {
+ return p.parent.getRequest()
+ }
+ return &http.Request{}
}
func (p *Partial) getFS() fs.FS {
@@ -416,33 +470,78 @@ func (p *Partial) getFS() fs.FS {
return nil
}
-func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request, renderTarget string) (template.HTML, error) {
- if renderTarget == "" || renderTarget == p.id {
- out, err := p.renderSelf(ctx, r.URL)
+func (p *Partial) getLogger() Logger {
+ if p == nil {
+ return slog.Default().WithGroup("partial")
+ }
+
+ if p.logger != nil {
+ return p.logger
+ }
+
+ if p.parent != nil {
+ return p.parent.getLogger()
+ }
+
+ // Cache the default logger in p.logger
+ p.logger = slog.Default().WithGroup("partial")
+
+ return p.logger
+}
+
+func (p *Partial) getRequestedPartial() string {
+ if p.requestedPartial != "" {
+ return p.requestedPartial
+ }
+ if p.parent != nil {
+ return p.parent.getRequestedPartial()
+ }
+ return ""
+}
+
+func (p *Partial) getRequestedAction() string {
+ if p.requestedAction != "" {
+ return p.requestedAction
+ }
+ if p.parent != nil {
+ return p.parent.getRequestedAction()
+ }
+ return ""
+}
+
+func (p *Partial) getRequestedSelect() string {
+ if p.requestedSelect != "" {
+ return p.requestedSelect
+ }
+ if p.parent != nil {
+ 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 {
+ out, err := p.renderSelf(ctx, r)
if err != nil {
return "", err
}
// Render OOB children of parent if necessary
if p.parent != nil {
- oobOut, oobErr := p.parent.renderOOBChildren(ctx, r.URL, true)
+ oobOut, oobErr := p.parent.renderOOBChildren(ctx, r, true)
if oobErr != nil {
- if p.logger != nil {
- p.logger.Error("error rendering OOB children of parent", "error", oobErr, "parent", p.parent.id)
- }
+ p.getLogger().Error("error rendering OOB children of parent", "error", oobErr, "parent", p.parent.id)
return "", fmt.Errorf("error rendering OOB children of parent with ID '%s': %w", p.parent.id, oobErr)
}
out += oobOut
}
return out, nil
} else {
- c := p.recursiveChildLookup(renderTarget, make(map[string]bool))
+ c := p.recursiveChildLookup(p.getRequestedPartial(), make(map[string]bool))
if c == nil {
- if p.logger != nil {
- p.logger.Error("requested partial not found in parent", "id", renderTarget, "parent", p.id)
- }
- return "", fmt.Errorf("requested partial %s not found in parent %s", renderTarget, p.id)
+ 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)
}
- return c.renderWithTarget(ctx, r, renderTarget)
+ return c.renderWithTarget(ctx, r)
}
}
@@ -474,9 +573,7 @@ func (p *Partial) renderChildPartial(ctx context.Context, id string, data map[st
child, ok := p.children[id]
p.mu.RUnlock()
if !ok {
- if p.logger != nil {
- p.logger.Warn("child partial not found", "id", id)
- }
+ p.getLogger().Warn("child partial not found", "id", id)
return "", nil
}
@@ -492,18 +589,21 @@ func (p *Partial) renderChildPartial(ctx context.Context, id string, data map[st
}
// Render the cloned child partial
- return childClone.renderSelf(ctx, nil)
+ return childClone.renderSelf(ctx, p.getRequest())
}
// renderNamed renders the partial with the given name and templates.
-func (p *Partial) renderSelf(ctx context.Context, currentURL *url.URL) (template.HTML, error) {
+func (p *Partial) renderSelf(ctx context.Context, r *http.Request) (template.HTML, error) {
if len(p.templates) == 0 {
- if p.logger != nil {
- p.logger.Error("no templates provided for rendering")
- }
+ p.getLogger().Error("no templates provided for rendering")
return "", errors.New("no templates provided for rendering")
}
+ var currentURL *url.URL
+ if r != nil {
+ currentURL = r.URL
+ }
+
data := &Data{
URL: currentURL,
Ctx: ctx,
@@ -512,30 +612,35 @@ func (p *Partial) renderSelf(ctx context.Context, currentURL *url.URL) (template
Layout: p.getLayoutData(),
}
+ if p.action != nil {
+ actionPartial, err := p.action(ctx, data)
+ if err != nil {
+ 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)
funcMapPtr := reflect.ValueOf(functions).Pointer()
cacheKey := p.generateCacheKey(p.templates, funcMapPtr)
tmpl, err := p.getOrParseTemplate(cacheKey, functions)
if err != nil {
- if p.logger != nil {
- p.logger.Error("error getting or parsing template", "error", err)
- }
+ p.getLogger().Error("error getting or parsing template", "error", err)
return "", err
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, data); err != nil {
- if p.logger != nil {
- p.logger.Error("error executing template", "template", p.templates[0], "error", err)
- }
+ p.getLogger().Error("error executing template", "template", p.templates[0], "error", err)
return "", fmt.Errorf("error executing template '%s': %w", p.templates[0], err)
}
return template.HTML(buf.String()), nil
}
-func (p *Partial) renderOOBChildren(ctx context.Context, currentURL *url.URL, swapOOB bool) (template.HTML, error) {
+func (p *Partial) renderOOBChildren(ctx context.Context, r *http.Request, swapOOB bool) (template.HTML, error) {
var out template.HTML
p.mu.RLock()
defer p.mu.RUnlock()
@@ -543,7 +648,7 @@ func (p *Partial) renderOOBChildren(ctx context.Context, currentURL *url.URL, sw
for id := range p.oobChildren {
if child, ok := p.children[id]; ok {
child.swapOOB = swapOOB
- childData, err := child.renderSelf(ctx, currentURL)
+ childData, err := child.renderSelf(ctx, r)
if err != nil {
return "", fmt.Errorf("error rendering OOB child '%s': %w", id, err)
}
@@ -601,12 +706,16 @@ func (p *Partial) clone() *Partial {
clone := &Partial{
id: p.id,
parent: p.parent,
+ request: p.request,
swapOOB: p.swapOOB,
fs: p.fs,
logger: p.logger,
partialHeader: p.partialHeader,
- requestHeader: p.requestHeader,
+ selectHeader: p.selectHeader,
+ actionHeader: p.actionHeader,
+ requestedPartial: p.requestedPartial,
useCache: p.useCache,
+ selection: p.selection,
templates: append([]string{}, p.templates...), // Copy the slice
combinedFunctions: make(template.FuncMap),
data: make(map[string]any),
diff --git a/partial_test.go b/partial_test.go
index 6b684da..2d6b941 100644
--- a/partial_test.go
+++ b/partial_test.go
@@ -3,6 +3,7 @@ package partial
import (
"context"
"html/template"
+ "io"
"net/http"
"net/http/httptest"
"strings"
@@ -419,6 +420,155 @@ func TestDataInTemplates(t *testing.T) {
}
}
+func TestWithSelectMap(t *testing.T) {
+ fsys := &InMemoryFS{
+ Files: map[string]string{
+ "index.gohtml": `{{ child "content" }}`,
+ "content.gohtml": `{{selection}}
`,
+ "tab1.gohtml": "Tab 1 Content",
+ "tab2.gohtml": "Tab 2 Content",
+ "default.gohtml": "Default Tab Content",
+ },
+ }
+
+ // Create a map of selection keys to partials
+ partialsMap := map[string]*Partial{
+ "tab1": New("tab1.gohtml").ID("tab1"),
+ "tab2": New("tab2.gohtml").ID("tab2"),
+ "default": New("default.gohtml").ID("default"),
+ }
+
+ // Create the content partial with the selection map
+ contentPartial := New("content.gohtml").
+ ID("content").
+ WithSelectMap("default", partialsMap)
+
+ // Create the layout partial
+ index := New("index.gohtml")
+
+ // Set up the service and layout
+ svc := NewService(&Config{
+ fs: fsys, // Set the file system in the service config
+ })
+ layout := svc.NewLayout().
+ Set(contentPartial).
+ Wrap(index)
+
+ // Set up a test server
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ err := layout.WriteWithRequest(ctx, w, r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ })
+
+ // Create a test server
+ server := httptest.NewServer(handler)
+ defer server.Close()
+
+ // Define test cases
+ testCases := []struct {
+ name string
+ selectHeader string
+ expectedContent string
+ }{
+ {
+ name: "Select tab1",
+ selectHeader: "tab1",
+ expectedContent: "Tab 1 Content",
+ },
+ {
+ name: "Select tab2",
+ selectHeader: "tab2",
+ expectedContent: "Tab 2 Content",
+ },
+ {
+ name: "Select default",
+ selectHeader: "",
+ expectedContent: "Default Tab Content",
+ },
+ {
+ name: "Invalid selection",
+ selectHeader: "invalid",
+ expectedContent: "selected partial 'invalid' not found in parent 'content'",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req, err := http.NewRequest("GET", server.URL, nil)
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ if tc.selectHeader != "" {
+ req.Header.Set("X-Select", tc.selectHeader)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("Failed to send request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // Read response body
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("Failed to read response body: %v", err)
+ }
+ bodyString := string(bodyBytes)
+
+ // Check if the expected content is in the response
+ if !strings.Contains(bodyString, tc.expectedContent) {
+ t.Errorf("Expected response to contain %q, but got %q", tc.expectedContent, bodyString)
+ }
+ })
+ }
+}
+
+func BenchmarkWithSelectMap(b *testing.B) {
+ fsys := &InMemoryFS{
+ Files: map[string]string{
+ "index.gohtml": `{{ child "content" }}`,
+ "content.gohtml": `{{selection}}
`,
+ "tab1.gohtml": "Tab 1 Content",
+ "tab2.gohtml": "Tab 2 Content",
+ "default.gohtml": "Default Tab Content",
+ },
+ }
+
+ service := NewService(&Config{
+ PartialHeader: "X-Partial",
+ UseCache: false,
+ })
+ layout := service.NewLayout().FS(fsys)
+
+ content := New("content.gohtml").
+ ID("content").
+ WithSelectMap("default", map[string]*Partial{
+ "tab1": New("tab1.gohtml").ID("tab1"),
+ "tab2": New("tab2.gohtml").ID("tab2"),
+ "default": New("default.gohtml").ID("default"),
+ })
+
+ index := New("index.gohtml")
+
+ layout.Set(content).Wrap(index)
+
+ req := httptest.NewRequest("GET", "/", nil)
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ // Call the function you want to benchmark
+ _, err := layout.RenderWithRequest(context.Background(), req)
+ if err != nil {
+ b.Fatalf("Error rendering: %v", err)
+ }
+ }
+}
+
func BenchmarkRenderWithRequest(b *testing.B) {
// Setup configuration and service
cfg := &Config{
@@ -426,18 +576,6 @@ func BenchmarkRenderWithRequest(b *testing.B) {
UseCache: false,
}
- // Benchmark results before function passing
- // with cache : BenchmarkRenderWithRequest-12 169927 6551 ns/op
- // without cache : BenchmarkRenderWithRequest-12 51270 22398 ns/op
-
- // Benchmark results after function passing
- // with cache : BenchmarkRenderWithRequest-12 65800 2240 ns/op
- // without cache : BenchmarkRenderWithRequest-12 42045 17857 ns/op
-
- // Benchmark results after function passing and optimization
- // with cache : BenchmarkRenderWithRequest-12 530058 2240 ns/op
- // without cache : BenchmarkRenderWithRequest-12 65800 17857 ns/op
-
service := NewService(cfg)
fsys := &InMemoryFS{
@@ -457,8 +595,10 @@ func BenchmarkRenderWithRequest(b *testing.B) {
"Message": "This is a benchmark test.",
})
+ index := NewID("index", "templates/index.html")
+
// Set the content partial in the layout
- layout.Set(content)
+ layout.Set(content).Wrap(index)
// Create a sample HTTP request
req := httptest.NewRequest("GET", "/", nil)
diff --git a/service.go b/service.go
index 5610c2d..6863912 100644
--- a/service.go
+++ b/service.go
@@ -12,6 +12,10 @@ import (
var (
// defaultPartialHeader is the default header used to determine which partial to render.
defaultPartialHeader = "X-Partial"
+ // defaultSelectHeader is the default header used to determine which partial to select.
+ defaultSelectHeader = "X-Select"
+ // defaultActionHeader is the default header used to determine which action to take.
+ defaultActionHeader = "X-Action"
)
type (
@@ -22,6 +26,8 @@ type (
Config struct {
PartialHeader string
+ SelectHeader string
+ ActionHeader string
UseCache bool
FuncMap template.FuncMap
Logger Logger
@@ -41,7 +47,10 @@ type (
content *Partial
wrapper *Partial
data map[string]any
- requestHeader string
+ requestedPartial string
+ requestedAction string
+ requestedSelect string
+ request *http.Request
combinedFunctions template.FuncMap
funcMapLock sync.RWMutex // Add a read-write mutex
}
@@ -57,6 +66,14 @@ func NewService(cfg *Config) *Service {
cfg.PartialHeader = defaultPartialHeader
}
+ if cfg.SelectHeader == "" {
+ cfg.SelectHeader = defaultSelectHeader
+ }
+
+ if cfg.ActionHeader == "" {
+ cfg.ActionHeader = defaultActionHeader
+ }
+
if cfg.Logger == nil {
cfg.Logger = slog.Default().WithGroup("partial")
}
@@ -168,18 +185,16 @@ 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) {
- // get partial header from request
- header := r.Header.Get(l.service.config.PartialHeader)
- // add header to data
- l.requestHeader = header
+ 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 {
- l.wrapper.requestHeader = l.requestHeader
l.wrapper.With(l.content)
// Render the wrapper
return l.wrapper.RenderWithRequest(ctx, r)
} else {
- l.content.requestHeader = l.requestHeader
// Render the content directly
return l.content.RenderWithRequest(ctx, r)
}
@@ -221,6 +236,9 @@ func (l *Layout) applyConfigToPartial(p *Partial) {
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.requestHeader = l.requestHeader
+ 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 3460eeb..7e651ef 100644
--- a/template_functions.go
+++ b/template_functions.go
@@ -144,3 +144,90 @@ func parseDate(layout, value string) (time.Time, error) {
func debug(v any) string {
return fmt.Sprintf("%+v", v)
}
+
+func selectionFunc(p *Partial, data *Data) func() template.HTML {
+ return func() template.HTML {
+ var selectedPartial *Partial
+
+ partials := p.getSelectionPartials()
+ if partials == nil {
+ p.getLogger().Error("no selection partials found", "id", p.id)
+ return template.HTML(fmt.Sprintf("no selection partials found in parent '%s'", p.id))
+ }
+
+ requestedSelect := p.getRequestedSelect()
+ if requestedSelect != "" {
+ selectedPartial = partials[requestedSelect]
+ } else {
+ selectedPartial = partials[p.selection.Default]
+ }
+
+ if selectedPartial == nil {
+ p.getLogger().Error("selected partial not found", "id", requestedSelect, "parent", p.id)
+ return template.HTML(fmt.Sprintf("selected partial '%s' not found in parent '%s'", requestedSelect, p.id))
+ }
+
+ selectedPartial.fs = p.fs
+ selectedPartial.parent = p
+
+ html, err := selectedPartial.renderSelf(data.Ctx, p.getRequest())
+ if err != nil {
+ p.getLogger().Error("error rendering selected partial", "id", requestedSelect, "parent", p.id, "error", err)
+ return template.HTML(fmt.Sprintf("error rendering selected partial '%s'", requestedSelect))
+ }
+
+ return html
+ }
+}
+
+func childFunc(p *Partial, data *Data) func(id string, vals ...any) template.HTML {
+ return func(id string, vals ...any) template.HTML {
+ if len(vals) > 0 && len(vals)%2 != 0 {
+ p.getLogger().Warn("invalid child data for partial, they come in key-value pairs", "id", id)
+ return template.HTML(fmt.Sprintf("invalid child data for partial '%s'", id))
+ }
+
+ d := make(map[string]any)
+ for i := 0; i < len(vals); i += 2 {
+ key, ok := vals[i].(string)
+ if !ok {
+ p.getLogger().Warn("invalid child data key for partial, it must be a string", "id", id, "key", vals[i])
+ return template.HTML(fmt.Sprintf("invalid child data key for partial '%s', want string, got %T", id, vals[i]))
+ }
+ d[key] = vals[i+1]
+ }
+
+ html, err := p.renderChildPartial(data.Ctx, id, d)
+ if err != nil {
+ p.getLogger().Error("error rendering partial", "id", id, "error", err)
+ // Handle error: you can log it and return an empty string or an error message
+ return template.HTML(fmt.Sprintf("error rendering partial '%s': %v", id, err))
+ }
+
+ return html
+ }
+}
+
+func actionFunc(p *Partial, data *Data) func() template.HTML {
+ return func() template.HTML {
+ if p.templateAction == nil {
+ p.getLogger().Error("no action callback found", "id", p.id)
+ return template.HTML(fmt.Sprintf("no action callback found in partial '%s'", p.id))
+ }
+
+ // Use the selector to get the appropriate partial
+ actionPartial, err := p.templateAction(data.Ctx, data)
+ if err != nil {
+ p.getLogger().Error("error in selector function", "error", err)
+ return template.HTML(fmt.Sprintf("error in action function: %v", err))
+ }
+
+ // Render the selected partial instead
+ html, err := actionPartial.renderSelf(data.Ctx, p.getRequest())
+ if err != nil {
+ p.getLogger().Error("error rendering action partial", "error", err)
+ return template.HTML(fmt.Sprintf("error rendering action partial: %v", err))
+ }
+ return html
+ }
+}