partial

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 4, 2024 License: MIT Imports: 17 Imported by: 0

README

Go Partial - Partial Page Rendering for Go

This package provides a flexible and efficient way to manage and render partial templates in Go (Golang). It allows you to create reusable, hierarchical templates with support for layouts, global data, caching, and more.

Features

  • Partial Templates: Define and render partial templates with their own data and functions.
  • Layouts: Use layouts to wrap content and share data across multiple partials.
  • Global Data: Set global data accessible to all partials.
  • Template Caching: Enable caching of parsed templates for improved performance.
  • Out-of-Band Rendering: Support for rendering out-of-band (OOB) partials.
  • File System Support: Use custom fs.FS implementations for template file access.
  • Thread-Safe: Designed for concurrent use in web applications.

Installation

To install the package, run:

go get github.com/donseba/go-partial

Advanced use cases

Advanced usecases are documented in the ADVANCED.md file

Integrations

Several integrations are available, detailed information can be found in the 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.

cfg := &partial.Config{
    PartialHeader: "X-Target",          // Optional: Header to determine which partial to render
    UseCache:      true,                 // Enable template caching
    FuncMap:       template.FuncMap{},   // Global template functions
    Logger:        myLogger,             // Implement the Logger interface or use nil
}

service := partial.NewService(cfg)
service.SetData(map[string]any{
    "AppName": "My Application",
})

2. Create a Layout

The Layout manages the overall structure of your templates.

layout := service.NewLayout()
layout.SetData(map[string]any{
    "PageTitle": "Home Page",
})
3. Define Partials

Create Partial instances for the content and any other components.

func handler(w http.ResponseWriter, r *http.Request) {
    // Create the main content partial
    content := partial.NewID("content", "templates/content.html")
    content.SetData(map[string]any{
        "Message": "Welcome to our website!",
    })
    
    // Optionally, create a wrapper partial (layout)
    wrapper := partial.NewID("wrapper", "templates/layout.html")
    
    layout.Set(content)
    layout.Wrap(wrapper)
    
    output, err := layout.RenderWithRequest(r.Context(), r)
    if err != nil {
        http.Error(w, "An error occurred while rendering the page.", http.StatusInternalServerError)
        return
    }
    w.Write([]byte(output))
}

Template Files

templates/layout.html

<!DOCTYPE html>
<html>
<head>
    <title>{{.Layout.PageTitle}} - {{.Service.AppName}}</title>
</head>
<body>
    {{ child "content" }}
</body>
</html>

templates/content.html

<h1>{{.Data.Message}}</h1>

Note: In the layout template, we use {{ child "content" }} to render the content partial on demand.

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.
Accessing Data in Templates

You can access data in your templates using dot notation:

  • Partial Data: {{ .Data.Key }}
  • Layout Data: {{ .Layout.Key }}
  • Global Data: {{ .Service.Key }}
Wrapping Partials

You can wrap a partial with another partial, such as wrapping content with a layout:

// Create the wrapper partial (e.g., layout)
layout := partial.New("templates/layout.html").ID("layout")

// Wrap the content partial with the layout
content.Wrap(layout)

Rendering Child Partials on Demand

Use the child function to render child partials within your templates.

Syntax
{{ child "partial_id" }}

You can also pass data to the child partial using key-value pairs:

{{ child "sidebar" "UserName" .Data.UserName "Notifications" .Data.Notifications }}

Child Partial (sidebar):

<div>
    <p>User: {{ .Data.UserName }}</p>
    <p>Notifications: {{ .Data.Notifications }}</p>
</div>

Using Out-of-Band (OOB) Partials

Out-of-Band partials allow you to update parts of the page without reloading:

Defining an OOB Partial
// Create the OOB partial
footer := partial.New("templates/footer.html").ID("footer")
footer.SetData(map[string]any{
    "Text": "This is the footer",
})

// Add the OOB partial
p.WithOOB(footer)
Using OOB Partials in Templates

In your templates, you can use the swapOOB function to conditionally render OOB attributes.

templates/footer.html

<div {{ if swapOOB }}hx-swap-oob="true"{{ end }} id="footer">{{ .Data.Text }}</div>

Wrapping Partials

You can wrap a partial with another partial, such as wrapping content with a layout.

// Create the wrapper partial (e.g., layout)
layoutPartial := partial.New("templates/layout.html").ID("layout")

// Wrap the content partial with the layout
content.Wrap(layoutPartial)

Template Functions

You can add custom functions to be used within your templates:

import "strings"

// Define custom functions
funcs := template.FuncMap{
    "upper": strings.ToUpper,
}

// Set the functions for the partial
p.SetFuncs(funcs)
Usage in Template:
{{ upper .Data.Message }}
Using a Custom File System

If your templates are stored in a custom file system, you can set it using WithFS:

import (
    "embed"
)

//go:embed templates/*
var content embed.FS

p.WithFS(content)

If you do not use a custom file system, the package will use the default file system and look for templates relative to the current working directory.

Rendering Tables and Dynamic Content

You can render dynamic content like tables by rendering child partials within loops.

Example: Rendering a Table with Dynamic Rows

templates/table.html

<table>
    {{ range $i := .Data.Rows }}
    {{ child "row" "RowNumber" $i }}
    {{ end }}
</table>

templates/row.html

<tr>
    <td>{{ .Data.RowNumber }}</td>
</tr>

Go Code:

// Create the row partial
rowPartial := partial.New("templates/row.html").ID("row")

// Create the table partial and set data
tablePartial := partial.New("templates/table.html").ID("table")
tablePartial.SetData(map[string]any{
"Rows": []int{1, 2, 3, 4, 5}, // Generate 5 rows
})
tablePartial.With(rowPartial)

// Render the table partial
out, err := layout.Set(tablePartial).RenderWithRequest(r.Context(), r)

Template Data

In your templates, you can access the following data:

  • {{.Ctx}}: The context of the request.
  • {{.URL}}: The URL of the request.
  • {{.Data}}: Data specific to this partial.
  • {{.Service}}: Global data available to all partials.
  • {{.Layout}}: Data specific to the layout.

Concurrency and Template Caching

The package includes concurrency safety measures for template caching:

  • Templates are cached using a sync.Map.
  • Mutexes are used to prevent race conditions during template parsing.
  • Set UseTemplateCache to true to enable template caching.
cfg := &partial.Config{
    UseCache: true,
}

Handling Partial Rendering via HTTP Headers

You can render specific partials based on the X-Target header (or your custom header).

Example:

func handler(w http.ResponseWriter, r *http.Request) {
    output, err := layout.RenderWithRequest(r.Context(), r)
    if err != nil {
        http.Error(w, "An error occurred while rendering the page.", http.StatusInternalServerError)
        return
    }
    w.Write([]byte(output))
}

To request a specific partial:

curl -H "X-Target: sidebar" http://localhost:8080

Useless benchmark results

with caching enabled

goos: darwin
goarch: arm64
pkg: github.com/donseba/go-partial
cpu: Apple M2 Pro
BenchmarkRenderWithRequest
BenchmarkRenderWithRequest-12    	  526102	      2254 ns/op
PASS

with caching disabled

goos: darwin
goarch: arm64
pkg: github.com/donseba/go-partial
cpu: Apple M2 Pro
BenchmarkRenderWithRequest
BenchmarkRenderWithRequest-12    	   57529	     19891 ns/op
PASS

which would mean that caching is rougly 9-10 times faster than without caching

Contributing

Contributions are welcome! Please open an issue or submit a pull request with your improvements.

License

This project is licensed under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultTemplateFuncMap = template.FuncMap{
	"safeHTML": safeHTML,

	"upper":       strings.ToUpper,
	"lower":       strings.ToLower,
	"trimSpace":   strings.TrimSpace,
	"trim":        strings.Trim,
	"trimSuffix":  strings.TrimSuffix,
	"trimPrefix":  strings.TrimPrefix,
	"contains":    strings.Contains,
	"containsAny": strings.ContainsAny,
	"hasPrefix":   strings.HasPrefix,
	"hasSuffix":   strings.HasSuffix,
	"repeat":      strings.Repeat,
	"replace":     strings.Replace,
	"split":       strings.Split,
	"join":        strings.Join,
	"stringSlice": stringSlice,
	"title":       title,
	"substr":      substr,
	"ucfirst":     ucfirst,
	"compare":     strings.Compare,
	"equalFold":   strings.EqualFold,
	"urlencode":   url.QueryEscape,
	"urldecode":   url.QueryUnescape,

	"now":        time.Now,
	"formatDate": formatDate,
	"parseDate":  parseDate,

	"first": first,
	"last":  last,

	"hasKey": hasKey,
	"keys":   keys,

	"debug": debug,
}

Functions

This section is empty.

Types

type Config added in v0.2.0

type Config struct {
	Connector connector.Connector
	UseCache  bool
	FuncMap   template.FuncMap
	Logger    Logger
	// contains filtered or unexported fields
}

type Data

type Data struct {
	// Ctx is the context of the request
	Ctx context.Context
	// URL is the URL of the request
	URL *url.URL
	// Request contains the http.Request
	Request *http.Request
	// Data contains the data specific to this partial
	Data map[string]any
	// Service contains global data available to all partials
	Service map[string]any
	// LayoutData contains data specific to the service
	Layout map[string]any
}

Data represents the data available to the partial.

type GlobalData

type GlobalData map[string]any

GlobalData represents the global data available to all partials.

type InMemoryFS

type InMemoryFS struct {
	Files map[string]string
}

func (*InMemoryFS) AddFile

func (f *InMemoryFS) AddFile(name, content string)

func (*InMemoryFS) Open

func (f *InMemoryFS) Open(name string) (fs.File, error)

type InMemoryFile

type InMemoryFile struct {
	*strings.Reader
	// contains filtered or unexported fields
}

func (*InMemoryFile) Close

func (f *InMemoryFile) Close() error

func (*InMemoryFile) ReadDir

func (f *InMemoryFile) ReadDir(count int) ([]fs.DirEntry, error)

func (*InMemoryFile) Stat

func (f *InMemoryFile) Stat() (fs.FileInfo, error)

type InMemoryFileInfo

type InMemoryFileInfo struct {
	// contains filtered or unexported fields
}

func (*InMemoryFileInfo) IsDir

func (fi *InMemoryFileInfo) IsDir() bool

func (*InMemoryFileInfo) ModTime

func (fi *InMemoryFileInfo) ModTime() time.Time

func (*InMemoryFileInfo) Mode

func (fi *InMemoryFileInfo) Mode() fs.FileMode

func (*InMemoryFileInfo) Name

func (fi *InMemoryFileInfo) Name() string

func (*InMemoryFileInfo) Size

func (fi *InMemoryFileInfo) Size() int64

func (*InMemoryFileInfo) Sys

func (fi *InMemoryFileInfo) Sys() interface{}

type Layout added in v0.2.0

type Layout struct {
	// contains filtered or unexported fields
}

func (*Layout) AddData added in v0.2.0

func (l *Layout) AddData(key string, value any) *Layout

AddData adds data to the layout.

func (*Layout) FS added in v0.2.0

func (l *Layout) FS(fs fs.FS) *Layout

FS sets the filesystem for the Layout.

func (*Layout) MergeFuncMap added in v0.2.0

func (l *Layout) MergeFuncMap(funcMap template.FuncMap)

MergeFuncMap merges the given FuncMap with the existing FuncMap in the Layout.

func (*Layout) RenderWithRequest added in v0.2.0

func (l *Layout) RenderWithRequest(ctx context.Context, r *http.Request) (template.HTML, error)

RenderWithRequest renders the partial with the given http.Request.

func (*Layout) Set added in v0.2.0

func (l *Layout) Set(p *Partial) *Layout

Set sets the content for the layout.

func (*Layout) SetData added in v0.2.0

func (l *Layout) SetData(data map[string]any) *Layout

SetData sets the data for the layout.

func (*Layout) Wrap added in v0.2.0

func (l *Layout) Wrap(p *Partial) *Layout

Wrap sets the wrapper for the layout.

func (*Layout) WriteWithRequest added in v0.2.0

func (l *Layout) WriteWithRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error

WriteWithRequest writes the layout to the response writer.

type Logger added in v0.2.0

type Logger interface {
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

type Node

type Node struct {
	ID    string
	Depth int
	Nodes []*Node
}

func Tree

func Tree(p *Partial) *Node

Tree returns the tree of partials.

type Partial

type Partial struct {
	// contains filtered or unexported fields
}

Partial represents a renderable component with optional children and data.

func New

func New(templates ...string) *Partial

New creates a new root.

func NewID

func NewID(id string, templates ...string) *Partial

NewID creates a new instance with the provided ID.

func (*Partial) AddData

func (p *Partial) AddData(key string, value any) *Partial

AddData adds data to the partial.

func (*Partial) AddFunc

func (p *Partial) AddFunc(name string, fn interface{}) *Partial

AddFunc adds a function to the partial.

func (*Partial) AddTemplate

func (p *Partial) AddTemplate(template string) *Partial

AddTemplate adds a template to the partial.

func (*Partial) GetRequest added in v0.8.0

func (p *Partial) GetRequest() *http.Request

func (*Partial) GetRequestedAction added in v0.5.0

func (p *Partial) GetRequestedAction() string

func (*Partial) GetRequestedPartial added in v0.8.0

func (p *Partial) GetRequestedPartial() string

func (*Partial) GetRequestedSelect added in v0.8.0

func (p *Partial) GetRequestedSelect() string

func (*Partial) GetResponseHeaders added in v0.8.0

func (p *Partial) GetResponseHeaders() map[string]string

func (*Partial) ID

func (p *Partial) ID(id string) *Partial

ID sets the ID of the partial.

func (*Partial) MergeData added in v0.2.0

func (p *Partial) MergeData(data map[string]any, override bool) *Partial

MergeData merges the data into the partial.

func (*Partial) MergeFuncMap added in v0.2.0

func (p *Partial) MergeFuncMap(funcMap template.FuncMap)

MergeFuncMap merges the given FuncMap with the existing FuncMap in the Partial.

func (*Partial) Render added in v0.2.0

func (p *Partial) Render(ctx context.Context) (template.HTML, error)

Render renders the partial without requiring an http.Request. It can be used when you don't need access to the request data.

func (*Partial) RenderWithRequest

func (p *Partial) RenderWithRequest(ctx context.Context, r *http.Request) (template.HTML, error)

RenderWithRequest renders the partial with the given http.Request.

func (*Partial) Reset

func (p *Partial) Reset() *Partial

Reset resets the partial to its initial state.

func (*Partial) SetConnector added in v0.8.0

func (p *Partial) SetConnector(connector connector.Connector) *Partial

SetConnector sets the connector for the partial.

func (*Partial) SetData

func (p *Partial) SetData(data map[string]any) *Partial

SetData sets the data for the partial.

func (*Partial) SetFileSystem added in v0.2.0

func (p *Partial) SetFileSystem(fs fs.FS) *Partial

SetFileSystem sets the file system for the partial.

func (*Partial) SetGlobalData

func (p *Partial) SetGlobalData(data map[string]any) *Partial

SetGlobalData sets the global data for the partial.

func (*Partial) SetLayoutData added in v0.2.0

func (p *Partial) SetLayoutData(data map[string]any) *Partial

SetLayoutData sets the layout data for the partial.

func (*Partial) SetLogger added in v0.2.0

func (p *Partial) SetLogger(logger Logger) *Partial

SetLogger sets the logger for the partial.

func (*Partial) SetParent added in v0.2.0

func (p *Partial) SetParent(parent *Partial) *Partial

SetParent sets the parent of the partial.

func (*Partial) SetResponseHeaders added in v0.8.0

func (p *Partial) SetResponseHeaders(headers map[string]string) *Partial

func (*Partial) Templates

func (p *Partial) Templates(templates ...string) *Partial

Templates sets the templates for the partial.

func (*Partial) UseCache added in v0.2.0

func (p *Partial) UseCache(useCache bool) *Partial

UseCache sets the cache usage flag for the partial.

func (*Partial) With

func (p *Partial) With(child *Partial) *Partial

With adds a child partial to the partial.

func (*Partial) WithAction added in v0.4.0

func (p *Partial) WithAction(action func(ctx context.Context, p *Partial, data *Data) (*Partial, error)) *Partial

WithAction adds callback action to the partial, which can do some logic and return a partial to render.

func (*Partial) WithOOB

func (p *Partial) WithOOB(child *Partial) *Partial

WithOOB adds an out-of-band child partial to the partial.

func (*Partial) WithSelectMap added in v0.4.0

func (p *Partial) WithSelectMap(defaultKey string, partialsMap map[string]*Partial) *Partial

WithSelectMap adds a selection partial to the partial.

func (*Partial) WithTemplateAction added in v0.4.0

func (p *Partial) WithTemplateAction(templateAction func(ctx context.Context, p *Partial, data *Data) (*Partial, error)) *Partial

func (*Partial) WriteWithRequest added in v0.2.0

func (p *Partial) WriteWithRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error

WriteWithRequest writes the partial to the http.ResponseWriter.

type Selection added in v0.4.0

type Selection struct {
	Partials map[string]*Partial
	Default  string
}

type Service added in v0.2.0

type Service struct {
	// contains filtered or unexported fields
}

func NewService added in v0.2.0

func NewService(cfg *Config) *Service

NewService returns a new partial service.

func (*Service) AddData added in v0.2.0

func (svc *Service) AddData(key string, value any) *Service

AddData adds data to the Service.

func (*Service) MergeFuncMap added in v0.2.0

func (svc *Service) MergeFuncMap(funcMap template.FuncMap)

MergeFuncMap merges the given FuncMap with the existing FuncMap.

func (*Service) NewLayout added in v0.2.0

func (svc *Service) NewLayout() *Layout

NewLayout returns a new layout.

func (*Service) SetConnector added in v0.8.0

func (svc *Service) SetConnector(conn connector.Connector) *Service

func (*Service) SetData added in v0.2.0

func (svc *Service) SetData(data map[string]any) *Service

SetData sets the data for the Service.

Directories

Path Synopsis
examples
form command
infinitescroll command
tabs command
tabs-htmx command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL