Simplified template renderer parsing and execution.
This commit is contained in:
parent
cb43e08183
commit
cd4cc1693c
76
README.md
76
README.md
@ -70,6 +70,7 @@
|
||||
* [HTMX support](#htmx-support)
|
||||
* [Rendering the page](#rendering-the-page)
|
||||
* [Template renderer](#template-renderer)
|
||||
* [Custom functions](#custom-functions)
|
||||
* [Caching](#caching)
|
||||
* [Hot-reload for development](#hot-reload-for-development)
|
||||
* [File configuration](#file-configuration)
|
||||
@ -759,7 +760,7 @@ Many examples of its usage are available in the included examples:
|
||||
- All navigation links use [boost](https://htmx.org/docs/#boosting) which dynamically replaces the page content with an AJAX request, providing a SPA-like experience.
|
||||
- All forms use either [boost](https://htmx.org/docs/#boosting) or [hx-post](https://htmx.org/docs/#triggers) to submit via AJAX.
|
||||
- The mock search autocomplete modal uses [hx-get](https://htmx.org/docs/#targets) to fetch search results from the server via AJAX and update the UI.
|
||||
- The mock posts on the homepage/dashboard use [hx-get](https://htmx.org/docs/#targets) to fetch and page posts vi AJAX.
|
||||
- The mock posts on the homepage/dashboard use [hx-get](https://htmx.org/docs/#targets) to fetch and page posts via AJAX.
|
||||
|
||||
All of this can be easily accomplished without writing any JavaScript at all.
|
||||
|
||||
@ -814,35 +815,64 @@ func (c *Home) Get(ctx echo.Context) error {
|
||||
|
||||
The _template renderer_ is a _Service_ on the `Container` that aims to make template parsing and rendering easy and flexible. It is the mechanism that allows the `Page` to do [automatic template parsing](#automatic-template-parsing). The standard `html/template` is still the engine used behind the scenes. The code can be found in `services/template_renderer.go`.
|
||||
|
||||
While there are several methods available, the following is the primary one used:
|
||||
Here is an example of a complex rendering that uses multiple template files as well as an entire directory of template files:
|
||||
|
||||
`ParseAndExecute(cacheGroup, cacheID, baseName string, files []string, directories []string, data interface{})`
|
||||
```go
|
||||
buf, err = c.TemplateRenderer.
|
||||
Parse().
|
||||
Group("page").
|
||||
Key("home").
|
||||
Base("main").
|
||||
Files("layouts/main", "pages/home").
|
||||
Directories("components").
|
||||
Execute(data)
|
||||
```
|
||||
|
||||
This will do the following:
|
||||
- [Cache](#caching) the parsed template with a _group_ of `page` and _key_ of `home` so this parse only happens once
|
||||
- Set the _base template file_ as `main`
|
||||
- Include the templates `templates/layout/main.gohtml` and `templates/pages/home.gohtml`
|
||||
- Include all templates located within the directory `templates/components`
|
||||
- Include the [funcmap](#funcmap)
|
||||
- Execute the parsed template with `data` being passed in to the templates
|
||||
|
||||
Using the example from the [page rendering](#rendering-the-page), this is what the `Controller` will execute:
|
||||
|
||||
```go
|
||||
buf, err = c.TemplateRenderer.ParseAndExecute(
|
||||
"page",
|
||||
page.Name,
|
||||
page.Layout,
|
||||
[]string{
|
||||
buf, err = c.Container.TemplateRenderer.
|
||||
Parse().
|
||||
Group("page").
|
||||
Key(page.Name).
|
||||
Base(page.Layout).
|
||||
Files(
|
||||
fmt.Sprintf("layouts/%s", page.Layout),
|
||||
fmt.Sprintf("pages/%s", page.Name),
|
||||
},
|
||||
[]string{"components"},
|
||||
page,
|
||||
)
|
||||
).
|
||||
Directories("components").
|
||||
Execute(page)
|
||||
```
|
||||
|
||||
The parameters represent:
|
||||
- `cacheGroup`: The _group_ to cache the parsed templates in
|
||||
- `cacheID`: The _ID_ of the cache within the _group_
|
||||
- `baseName`: The name of the base template, excluding the extension
|
||||
- `files`: A list of individual template files to include, excluding the extension and template directory
|
||||
- `directories`: A list of directories to include all templates contained
|
||||
- `data`: The data object to send to the templates
|
||||
If you have a need to _separately_ parse and cache the templates then later execute, you can separate the operations:
|
||||
|
||||
All templates will be parsed with the [funcap](#funcmap).
|
||||
```go
|
||||
_, err := c.TemplateRenderer.
|
||||
Parse().
|
||||
Group("my-group").
|
||||
Key("my-key").
|
||||
Base("auth").
|
||||
Files("layouts/auth", "pages/login").
|
||||
Directories("components").
|
||||
Store()
|
||||
```
|
||||
|
||||
```go
|
||||
tpl, err := c.TemplateRenderer.Load("my-group", "my-key")
|
||||
buf, err := tpl.Execute(data)
|
||||
```
|
||||
|
||||
### Custom functions
|
||||
|
||||
All templates will be parsed with the [funcmap](#funcmap) so all of your custom functions as well as the functions provided by [sprig](https://github.com/Masterminds/sprig) will be available.
|
||||
|
||||
### Caching
|
||||
|
||||
@ -935,7 +965,7 @@ err := c.Cache.
|
||||
Flush().
|
||||
Group("my-group").
|
||||
Key("my-key").
|
||||
Exec(ctx)
|
||||
Execute(ctx)
|
||||
```
|
||||
|
||||
### Flush tags
|
||||
@ -946,7 +976,7 @@ This will flush all cache entries that were tagged with the given tags.
|
||||
err := c.Cache.
|
||||
Flush().
|
||||
Tags("tag1", "tag2").
|
||||
Exec(ctx)
|
||||
Execute(ctx)
|
||||
```
|
||||
|
||||
## Static files
|
||||
@ -979,7 +1009,7 @@ Where `9fhe73kaf3` is the randomly-generated cache-buster.
|
||||
|
||||
An email client was added as a _Service_ to the `Container` but it is just a skeleton without any actual email-sending functionality. The reason is because there are a lot of ways to send email and most prefer using a SaaS solution for that. That makes it difficult to provide a generic solution that will work for most applications.
|
||||
|
||||
The structure in the client (`MailClient`) makes composing emails very easy and you have the option to construct the body using either a simple string or with a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction. **You must** finish the implementation of `mail.Send`.
|
||||
The structure in the client (`MailClient`) makes composing emails very easy and you have the option to construct the body using either a simple string or with a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction. **You must** finish the implementation of `MailClient.send`.
|
||||
|
||||
The _from_ address will default to the configuration value at `Config.Mail.FromAddress`. This can be overridden per-email by calling `From()` on the email and passing in the desired address.
|
||||
|
||||
|
@ -52,17 +52,17 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
|
||||
// 2. The content template specified in Page.Name
|
||||
// 3. All templates within the components directory
|
||||
// Also included is the function map provided by the funcmap package
|
||||
buf, err = c.Container.TemplateRenderer.ParseAndExecute(
|
||||
"page:htmx",
|
||||
page.Name,
|
||||
"htmx",
|
||||
[]string{
|
||||
buf, err = c.Container.TemplateRenderer.
|
||||
Parse().
|
||||
Group("page:htmx").
|
||||
Key(page.Name).
|
||||
Base("htmx").
|
||||
Files(
|
||||
"htmx",
|
||||
fmt.Sprintf("pages/%s", page.Name),
|
||||
},
|
||||
[]string{"components"},
|
||||
page,
|
||||
)
|
||||
).
|
||||
Directories("components").
|
||||
Execute(page)
|
||||
} else {
|
||||
// Parse and execute the templates for the Page
|
||||
// As mentioned in the documentation for the Page struct, the templates used for the page will be:
|
||||
@ -70,17 +70,17 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
|
||||
// 2. The content template specified in Page.Name
|
||||
// 3. All templates within the components directory
|
||||
// Also included is the function map provided by the funcmap package
|
||||
buf, err = c.Container.TemplateRenderer.ParseAndExecute(
|
||||
"page",
|
||||
page.Name,
|
||||
page.Layout,
|
||||
[]string{
|
||||
buf, err = c.Container.TemplateRenderer.
|
||||
Parse().
|
||||
Group("page").
|
||||
Key(page.Name).
|
||||
Base(page.Layout).
|
||||
Files(
|
||||
fmt.Sprintf("layouts/%s", page.Layout),
|
||||
fmt.Sprintf("pages/%s", page.Name),
|
||||
},
|
||||
[]string{"components"},
|
||||
page,
|
||||
)
|
||||
).
|
||||
Directories("components").
|
||||
Execute(page)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -100,7 +100,7 @@ func TestController_RenderPage(t *testing.T) {
|
||||
expectedTemplates[f.Name()] = true
|
||||
}
|
||||
|
||||
for _, v := range parsed.Templates() {
|
||||
for _, v := range parsed.Template.Templates() {
|
||||
delete(expectedTemplates, v.Name())
|
||||
}
|
||||
assert.Empty(t, expectedTemplates)
|
||||
@ -133,7 +133,7 @@ func TestController_RenderPage(t *testing.T) {
|
||||
expectedTemplates[f.Name()] = true
|
||||
}
|
||||
|
||||
for _, v := range parsed.Templates() {
|
||||
for _, v := range parsed.Template.Templates() {
|
||||
delete(expectedTemplates, v.Name())
|
||||
}
|
||||
assert.Empty(t, expectedTemplates)
|
||||
@ -167,7 +167,7 @@ func TestController_RenderPage(t *testing.T) {
|
||||
err = c.Cache.
|
||||
Flush().
|
||||
Tags(p.Cache.Tags[0]).
|
||||
Exec(context.Background())
|
||||
Execute(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Refetch from the cache and expect no results
|
||||
|
@ -195,8 +195,8 @@ func (c *cacheFlush) Tags(tags ...string) *cacheFlush {
|
||||
return c
|
||||
}
|
||||
|
||||
// Exec flushes the data from the cache
|
||||
func (c *cacheFlush) Exec(ctx context.Context) error {
|
||||
// Execute flushes the data from the cache
|
||||
func (c *cacheFlush) Execute(ctx context.Context) error {
|
||||
if len(c.tags) > 0 {
|
||||
if err := c.client.cache.Invalidate(ctx, store.InvalidateOptions{
|
||||
Tags: c.tags,
|
||||
|
@ -51,7 +51,7 @@ func TestCacheClient(t *testing.T) {
|
||||
Flush().
|
||||
Group(group).
|
||||
Key(key).
|
||||
Exec(context.Background())
|
||||
Execute(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// The data should be gone
|
||||
@ -81,7 +81,7 @@ func TestCacheClient(t *testing.T) {
|
||||
err = c.Cache.
|
||||
Flush().
|
||||
Tags("tag1").
|
||||
Exec(context.Background())
|
||||
Execute(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// The data should be gone
|
||||
|
@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mikestefanello/pagoda/config"
|
||||
@ -21,6 +22,7 @@ type (
|
||||
templates *TemplateRenderer
|
||||
}
|
||||
|
||||
// mail represents an email to be sent
|
||||
mail struct {
|
||||
client *MailClient
|
||||
from string
|
||||
@ -53,6 +55,43 @@ func (m *MailClient) skipSend() bool {
|
||||
return m.config.App.Environment != config.EnvProduction
|
||||
}
|
||||
|
||||
// send attempts to send the email
|
||||
func (m *MailClient) send(email *mail, ctx echo.Context) error {
|
||||
switch {
|
||||
case email.to == "":
|
||||
return errors.New("email cannot be sent without a to address")
|
||||
case email.body == "" && email.template == "":
|
||||
return errors.New("email cannot be sent without a body or template")
|
||||
}
|
||||
|
||||
// Check if a template was supplied
|
||||
if email.template != "" {
|
||||
// Parse and execute template
|
||||
buf, err := m.templates.
|
||||
Parse().
|
||||
Group("mail").
|
||||
Key(email.template).
|
||||
Base(email.template).
|
||||
Files(fmt.Sprintf("emails/%s", email.template)).
|
||||
Execute(email.templateData)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
email.body = buf.String()
|
||||
}
|
||||
|
||||
// Check if mail sending should be skipped
|
||||
if m.skipSend() {
|
||||
ctx.Logger().Debugf("skipping email sent to: %s", email.to)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Finish based on your mail sender of choice!
|
||||
return nil
|
||||
}
|
||||
|
||||
// From sets the email from address
|
||||
func (m *mail) From(from string) *mail {
|
||||
m.from = from
|
||||
@ -96,30 +135,5 @@ func (m *mail) TemplateData(data interface{}) *mail {
|
||||
|
||||
// Send attempts to send the email
|
||||
func (m *mail) Send(ctx echo.Context) error {
|
||||
// Check if a template was supplied
|
||||
if m.template != "" {
|
||||
// Parse and execute template
|
||||
buf, err := m.client.templates.ParseAndExecute(
|
||||
"mail",
|
||||
m.template,
|
||||
m.template,
|
||||
[]string{fmt.Sprintf("emails/%s", m.template)},
|
||||
[]string{},
|
||||
m.templateData,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.body = buf.String()
|
||||
}
|
||||
|
||||
// Check if mail sending should be skipped
|
||||
if m.client.skipSend() {
|
||||
ctx.Logger().Debugf("skipping email sent to: %s", m.to)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Finish based on your mail sender of choice!
|
||||
return nil
|
||||
return m.client.send(m, ctx)
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ import (
|
||||
"github.com/mikestefanello/pagoda/funcmap"
|
||||
)
|
||||
|
||||
// TemplateRenderer provides a flexible and easy to use method of rendering simple templates or complex sets of
|
||||
// templates while also providing caching and/or hot-reloading depending on your current environment
|
||||
type TemplateRenderer struct {
|
||||
type (
|
||||
// TemplateRenderer provides a flexible and easy to use method of rendering simple templates or complex sets of
|
||||
// templates while also providing caching and/or hot-reloading depending on your current environment
|
||||
TemplateRenderer struct {
|
||||
// templateCache stores a cache of parsed page templates
|
||||
templateCache sync.Map
|
||||
|
||||
@ -28,7 +29,32 @@ type TemplateRenderer struct {
|
||||
|
||||
// config stores application configuration
|
||||
config *config.Config
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateParsed is a wrapper around parsed templates which are stored in the TemplateRenderer cache
|
||||
TemplateParsed struct {
|
||||
// Template is the parsed template
|
||||
Template *template.Template
|
||||
|
||||
// build stores the build data used to parse the template
|
||||
build *templateBuild
|
||||
}
|
||||
|
||||
// templateBuild stores the build data used to parse a template
|
||||
templateBuild struct {
|
||||
group string
|
||||
key string
|
||||
base string
|
||||
files []string
|
||||
directories []string
|
||||
}
|
||||
|
||||
// templateBuilder handles chaining a template parse operation
|
||||
templateBuilder struct {
|
||||
build *templateBuild
|
||||
renderer *TemplateRenderer
|
||||
}
|
||||
)
|
||||
|
||||
// NewTemplateRenderer creates a new TemplateRenderer
|
||||
func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer {
|
||||
@ -47,123 +73,12 @@ func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer {
|
||||
return t
|
||||
}
|
||||
|
||||
// Parse parses a set of templates and caches them for quick execution
|
||||
// If the application environment is set to local, the cache will be bypassed and templates will be
|
||||
// parsed upon each request so hot-reloading is possible without restarts.
|
||||
//
|
||||
// All template files and template directories must be provided relative to the templates directory
|
||||
// and without template extensions. Those two values can be altered via the config package.
|
||||
//
|
||||
// cacheGroup is used to separate templates in to groups within the cache to avoid potential conflicts
|
||||
// with the cacheID.
|
||||
//
|
||||
// baseName is the filename of the base template without any paths or an extension.
|
||||
// files is a slice of all individual template files that will be included in the parse.
|
||||
// directories is a slice of directories which all template files witin them will be included in the parse
|
||||
//
|
||||
// Also included will be the function map provided by the funcmap package.
|
||||
//
|
||||
// An example usage of this:
|
||||
// t.Parse(
|
||||
// "page",
|
||||
// "home",
|
||||
// "main",
|
||||
// []string{
|
||||
// "layouts/main",
|
||||
// "pages/home",
|
||||
// },
|
||||
// []string{"components"},
|
||||
//)
|
||||
//
|
||||
// This will perform a template parse which will:
|
||||
// - Be cached using a key of "page:home"
|
||||
// - Include the layouts/main.gohtml and pages/home.gohtml templates
|
||||
// - Include all templates within the components directory
|
||||
// - Include the function map within the funcmap package
|
||||
// - Set the base template as main.gohtml
|
||||
func (t *TemplateRenderer) Parse(cacheGroup, cacheID, baseName string, files []string, directories []string) error {
|
||||
cacheKey := t.getCacheKey(cacheGroup, cacheID)
|
||||
|
||||
// Check if the template has not yet been parsed or if the app environment is local, so that
|
||||
// templates reflect changes without having the restart the server
|
||||
if _, err := t.Load(cacheGroup, cacheID); err != nil || t.config.App.Environment == config.EnvLocal {
|
||||
// Initialize the parsed template with the function map
|
||||
parsed := template.New(baseName + config.TemplateExt).
|
||||
Funcs(t.funcMap)
|
||||
|
||||
// Parse all files provided
|
||||
if len(files) > 0 {
|
||||
for k, v := range files {
|
||||
files[k] = fmt.Sprintf("%s/%s%s", t.templatesPath, v, config.TemplateExt)
|
||||
// Parse creates a template build operation
|
||||
func (t *TemplateRenderer) Parse() *templateBuilder {
|
||||
return &templateBuilder{
|
||||
renderer: t,
|
||||
build: &templateBuild{},
|
||||
}
|
||||
|
||||
parsed, err = parsed.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse all templates within the provided directories
|
||||
for _, dir := range directories {
|
||||
dir = fmt.Sprintf("%s/%s/*%s", t.templatesPath, dir, config.TemplateExt)
|
||||
parsed, err = parsed.ParseGlob(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Store the template so this process only happens once
|
||||
t.templateCache.Store(cacheKey, parsed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute executes a cached template with the data provided
|
||||
// See Parse() for an explanation of the parameters
|
||||
func (t *TemplateRenderer) Execute(cacheGroup, cacheID, baseName string, data interface{}) (*bytes.Buffer, error) {
|
||||
tmpl, err := t.Load(cacheGroup, cacheID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = tmpl.ExecuteTemplate(buf, baseName+config.TemplateExt, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// ParseAndExecute is a wrapper around Parse() and Execute()
|
||||
func (t *TemplateRenderer) ParseAndExecute(cacheGroup, cacheID, baseName string, files []string, directories []string, data interface{}) (*bytes.Buffer, error) {
|
||||
var buf *bytes.Buffer
|
||||
var err error
|
||||
|
||||
if err = t.Parse(cacheGroup, cacheID, baseName, files, directories); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if buf, err = t.Execute(cacheGroup, cacheID, baseName, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Load loads a template from the cache
|
||||
func (t *TemplateRenderer) Load(cacheGroup, cacheID string) (*template.Template, error) {
|
||||
load, ok := t.templateCache.Load(t.getCacheKey(cacheGroup, cacheID))
|
||||
if !ok {
|
||||
return nil, errors.New("uncached page template requested")
|
||||
}
|
||||
|
||||
tmpl, ok := load.(*template.Template)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to cast cached template")
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// GetTemplatesPath gets the complete path to the templates directory
|
||||
@ -172,6 +87,147 @@ func (t *TemplateRenderer) GetTemplatesPath() string {
|
||||
}
|
||||
|
||||
// getCacheKey gets a cache key for a given group and ID
|
||||
func (t *TemplateRenderer) getCacheKey(cacheGroup, cacheID string) string {
|
||||
return fmt.Sprintf("%s:%s", cacheGroup, cacheID)
|
||||
func (t *TemplateRenderer) getCacheKey(group, key string) string {
|
||||
if group != "" {
|
||||
return fmt.Sprintf("%s:%s", group, key)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// parse parses a set of templates and caches them for quick execution
|
||||
// If the application environment is set to local, the cache will be bypassed and templates will be
|
||||
// parsed upon each request so hot-reloading is possible without restarts.
|
||||
// Also included will be the function map provided by the funcmap package.
|
||||
func (t *TemplateRenderer) parse(build *templateBuild) (*TemplateParsed, error) {
|
||||
var tp *TemplateParsed
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case build.key == "":
|
||||
return nil, errors.New("cannot parse template without key")
|
||||
case len(build.files) == 0 && len(build.directories) == 0:
|
||||
return nil, errors.New("cannot parse template without files or directories")
|
||||
case build.base == "":
|
||||
return nil, errors.New("cannot parse template without base")
|
||||
}
|
||||
|
||||
// Generate the cache key
|
||||
cacheKey := t.getCacheKey(build.group, build.key)
|
||||
|
||||
// Check if the template has not yet been parsed or if the app environment is local, so that
|
||||
// templates reflect changes without having the restart the server
|
||||
if tp, err = t.Load(build.group, build.key); err != nil || t.config.App.Environment == config.EnvLocal {
|
||||
// Initialize the parsed template with the function map
|
||||
parsed := template.New(build.base + config.TemplateExt).
|
||||
Funcs(t.funcMap)
|
||||
|
||||
// Parse all files provided
|
||||
if len(build.files) > 0 {
|
||||
for k, v := range build.files {
|
||||
build.files[k] = fmt.Sprintf("%s/%s%s", t.templatesPath, v, config.TemplateExt)
|
||||
}
|
||||
|
||||
parsed, err = parsed.ParseFiles(build.files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse all templates within the provided directories
|
||||
for _, dir := range build.directories {
|
||||
dir = fmt.Sprintf("%s/%s/*%s", t.templatesPath, dir, config.TemplateExt)
|
||||
parsed, err = parsed.ParseGlob(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Store the template so this process only happens once
|
||||
tp = &TemplateParsed{
|
||||
Template: parsed,
|
||||
build: build,
|
||||
}
|
||||
t.templateCache.Store(cacheKey, tp)
|
||||
}
|
||||
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
// Load loads a template from the cache
|
||||
func (t *TemplateRenderer) Load(group, key string) (*TemplateParsed, error) {
|
||||
load, ok := t.templateCache.Load(t.getCacheKey(group, key))
|
||||
if !ok {
|
||||
return nil, errors.New("uncached page template requested")
|
||||
}
|
||||
|
||||
tmpl, ok := load.(*TemplateParsed)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to cast cached template")
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// Execute executes a template with the given data and provides the output
|
||||
func (t *TemplateParsed) Execute(data interface{}) (*bytes.Buffer, error) {
|
||||
if t.Template == nil {
|
||||
return nil, errors.New("cannot execute template: template not initialized")
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := t.Template.ExecuteTemplate(buf, t.build.base+config.TemplateExt, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Group sets the cache group for the template being built
|
||||
func (t *templateBuilder) Group(group string) *templateBuilder {
|
||||
t.build.group = group
|
||||
return t
|
||||
}
|
||||
|
||||
// Key sets the cache key for the template being built
|
||||
func (t *templateBuilder) Key(key string) *templateBuilder {
|
||||
t.build.key = key
|
||||
return t
|
||||
}
|
||||
|
||||
// Base sets the name of the base template to be used during template parsing and execution.
|
||||
// This should be only the file name without a directory or extension.
|
||||
func (t *templateBuilder) Base(base string) *templateBuilder {
|
||||
t.build.base = base
|
||||
return t
|
||||
}
|
||||
|
||||
// Files sets a list of template files to include in the parse.
|
||||
// This should not include the file extension and the paths should be relative to the templates directory.
|
||||
func (t *templateBuilder) Files(files ...string) *templateBuilder {
|
||||
t.build.files = files
|
||||
return t
|
||||
}
|
||||
|
||||
// Directories sets a list of directories that all template files within will be parsed.
|
||||
// The paths should be relative to the templates directory.
|
||||
func (t *templateBuilder) Directories(directories ...string) *templateBuilder {
|
||||
t.build.directories = directories
|
||||
return t
|
||||
}
|
||||
|
||||
// Store parsed the templates and stores them in the cache
|
||||
func (t *templateBuilder) Store() (*TemplateParsed, error) {
|
||||
return t.renderer.parse(t.build)
|
||||
}
|
||||
|
||||
// Execute executes the template with the given data.
|
||||
// If the template has not already been cached, this will parse and cache the template
|
||||
func (t *templateBuilder) Execute(data interface{}) (*bytes.Buffer, error) {
|
||||
tp, err := t.Store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tp.Execute(data)
|
||||
}
|
||||
|
@ -19,13 +19,14 @@ func TestTemplateRenderer(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
|
||||
// Parse in to the cache
|
||||
err = c.TemplateRenderer.Parse(
|
||||
group,
|
||||
id,
|
||||
"htmx",
|
||||
[]string{"htmx", "pages/error"},
|
||||
[]string{"components"},
|
||||
)
|
||||
tpl, err := c.TemplateRenderer.
|
||||
Parse().
|
||||
Group(group).
|
||||
Key(id).
|
||||
Base("htmx").
|
||||
Files("htmx", "pages/error").
|
||||
Directories("components").
|
||||
Store()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should exist now
|
||||
@ -41,7 +42,7 @@ func TestTemplateRenderer(t *testing.T) {
|
||||
for _, f := range components {
|
||||
expectedTemplates[f.Name()] = true
|
||||
}
|
||||
for _, v := range parsed.Templates() {
|
||||
for _, v := range parsed.Template.Templates() {
|
||||
delete(expectedTemplates, v.Name())
|
||||
}
|
||||
assert.Empty(t, expectedTemplates)
|
||||
@ -51,19 +52,20 @@ func TestTemplateRenderer(t *testing.T) {
|
||||
}{
|
||||
StatusCode: 500,
|
||||
}
|
||||
buf, err := c.TemplateRenderer.Execute(group, id, "htmx", data)
|
||||
buf, err := tpl.Execute(data)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, buf)
|
||||
assert.Contains(t, buf.String(), "Please try again")
|
||||
|
||||
buf, err = c.TemplateRenderer.ParseAndExecute(
|
||||
group,
|
||||
id,
|
||||
"htmx",
|
||||
[]string{"htmx", "pages/error"},
|
||||
[]string{"components"},
|
||||
data,
|
||||
)
|
||||
buf, err = c.TemplateRenderer.
|
||||
Parse().
|
||||
Group(group).
|
||||
Key(id).
|
||||
Base("htmx").
|
||||
Files("htmx", "pages/error").
|
||||
Directories("components").
|
||||
Execute(data)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, buf)
|
||||
assert.Contains(t, buf.String(), "Please try again")
|
||||
|
Loading…
Reference in New Issue
Block a user