Simplified template renderer parsing and execution.

This commit is contained in:
mikestefanello 2022-01-19 09:14:18 -05:00
parent cb43e08183
commit cd4cc1693c
8 changed files with 323 additions and 221 deletions

View file

@ -70,6 +70,7 @@
* [HTMX support](#htmx-support) * [HTMX support](#htmx-support)
* [Rendering the page](#rendering-the-page) * [Rendering the page](#rendering-the-page)
* [Template renderer](#template-renderer) * [Template renderer](#template-renderer)
* [Custom functions](#custom-functions)
* [Caching](#caching) * [Caching](#caching)
* [Hot-reload for development](#hot-reload-for-development) * [Hot-reload for development](#hot-reload-for-development)
* [File configuration](#file-configuration) * [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 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. - 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 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. 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`. 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: Using the example from the [page rendering](#rendering-the-page), this is what the `Controller` will execute:
```go ```go
buf, err = c.TemplateRenderer.ParseAndExecute( buf, err = c.Container.TemplateRenderer.
"page", Parse().
page.Name, Group("page").
page.Layout, Key(page.Name).
[]string{ Base(page.Layout).
Files(
fmt.Sprintf("layouts/%s", page.Layout), fmt.Sprintf("layouts/%s", page.Layout),
fmt.Sprintf("pages/%s", page.Name), fmt.Sprintf("pages/%s", page.Name),
}, ).
[]string{"components"}, Directories("components").
page, Execute(page)
)
``` ```
The parameters represent: If you have a need to _separately_ parse and cache the templates then later execute, you can separate the operations:
- `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
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 ### Caching
@ -935,7 +965,7 @@ err := c.Cache.
Flush(). Flush().
Group("my-group"). Group("my-group").
Key("my-key"). Key("my-key").
Exec(ctx) Execute(ctx)
``` ```
### Flush tags ### Flush tags
@ -946,7 +976,7 @@ This will flush all cache entries that were tagged with the given tags.
err := c.Cache. err := c.Cache.
Flush(). Flush().
Tags("tag1", "tag2"). Tags("tag1", "tag2").
Exec(ctx) Execute(ctx)
``` ```
## Static files ## 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. 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. 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.

View file

@ -52,17 +52,17 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
// 2. The content template specified in Page.Name // 2. The content template specified in Page.Name
// 3. All templates within the components directory // 3. All templates within the components directory
// Also included is the function map provided by the funcmap package // Also included is the function map provided by the funcmap package
buf, err = c.Container.TemplateRenderer.ParseAndExecute( buf, err = c.Container.TemplateRenderer.
"page:htmx", Parse().
page.Name, Group("page:htmx").
"htmx", Key(page.Name).
[]string{ Base("htmx").
Files(
"htmx", "htmx",
fmt.Sprintf("pages/%s", page.Name), fmt.Sprintf("pages/%s", page.Name),
}, ).
[]string{"components"}, Directories("components").
page, Execute(page)
)
} else { } else {
// Parse and execute the templates for the Page // 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: // 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 // 2. The content template specified in Page.Name
// 3. All templates within the components directory // 3. All templates within the components directory
// Also included is the function map provided by the funcmap package // Also included is the function map provided by the funcmap package
buf, err = c.Container.TemplateRenderer.ParseAndExecute( buf, err = c.Container.TemplateRenderer.
"page", Parse().
page.Name, Group("page").
page.Layout, Key(page.Name).
[]string{ Base(page.Layout).
Files(
fmt.Sprintf("layouts/%s", page.Layout), fmt.Sprintf("layouts/%s", page.Layout),
fmt.Sprintf("pages/%s", page.Name), fmt.Sprintf("pages/%s", page.Name),
}, ).
[]string{"components"}, Directories("components").
page, Execute(page)
)
} }
if err != nil { if err != nil {

View file

@ -100,7 +100,7 @@ func TestController_RenderPage(t *testing.T) {
expectedTemplates[f.Name()] = true expectedTemplates[f.Name()] = true
} }
for _, v := range parsed.Templates() { for _, v := range parsed.Template.Templates() {
delete(expectedTemplates, v.Name()) delete(expectedTemplates, v.Name())
} }
assert.Empty(t, expectedTemplates) assert.Empty(t, expectedTemplates)
@ -133,7 +133,7 @@ func TestController_RenderPage(t *testing.T) {
expectedTemplates[f.Name()] = true expectedTemplates[f.Name()] = true
} }
for _, v := range parsed.Templates() { for _, v := range parsed.Template.Templates() {
delete(expectedTemplates, v.Name()) delete(expectedTemplates, v.Name())
} }
assert.Empty(t, expectedTemplates) assert.Empty(t, expectedTemplates)
@ -167,7 +167,7 @@ func TestController_RenderPage(t *testing.T) {
err = c.Cache. err = c.Cache.
Flush(). Flush().
Tags(p.Cache.Tags[0]). Tags(p.Cache.Tags[0]).
Exec(context.Background()) Execute(context.Background())
require.NoError(t, err) require.NoError(t, err)
// Refetch from the cache and expect no results // Refetch from the cache and expect no results

View file

@ -195,8 +195,8 @@ func (c *cacheFlush) Tags(tags ...string) *cacheFlush {
return c return c
} }
// Exec flushes the data from the cache // Execute flushes the data from the cache
func (c *cacheFlush) Exec(ctx context.Context) error { func (c *cacheFlush) Execute(ctx context.Context) error {
if len(c.tags) > 0 { if len(c.tags) > 0 {
if err := c.client.cache.Invalidate(ctx, store.InvalidateOptions{ if err := c.client.cache.Invalidate(ctx, store.InvalidateOptions{
Tags: c.tags, Tags: c.tags,

View file

@ -51,7 +51,7 @@ func TestCacheClient(t *testing.T) {
Flush(). Flush().
Group(group). Group(group).
Key(key). Key(key).
Exec(context.Background()) Execute(context.Background())
require.NoError(t, err) require.NoError(t, err)
// The data should be gone // The data should be gone
@ -81,7 +81,7 @@ func TestCacheClient(t *testing.T) {
err = c.Cache. err = c.Cache.
Flush(). Flush().
Tags("tag1"). Tags("tag1").
Exec(context.Background()) Execute(context.Background())
require.NoError(t, err) require.NoError(t, err)
// The data should be gone // The data should be gone

View file

@ -1,6 +1,7 @@
package services package services
import ( import (
"errors"
"fmt" "fmt"
"github.com/mikestefanello/pagoda/config" "github.com/mikestefanello/pagoda/config"
@ -21,6 +22,7 @@ type (
templates *TemplateRenderer templates *TemplateRenderer
} }
// mail represents an email to be sent
mail struct { mail struct {
client *MailClient client *MailClient
from string from string
@ -53,6 +55,43 @@ func (m *MailClient) skipSend() bool {
return m.config.App.Environment != config.EnvProduction 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 // From sets the email from address
func (m *mail) From(from string) *mail { func (m *mail) From(from string) *mail {
m.from = from m.from = from
@ -96,30 +135,5 @@ func (m *mail) TemplateData(data interface{}) *mail {
// Send attempts to send the email // Send attempts to send the email
func (m *mail) Send(ctx echo.Context) error { func (m *mail) Send(ctx echo.Context) error {
// Check if a template was supplied return m.client.send(m, ctx)
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
} }

View file

@ -14,21 +14,47 @@ import (
"github.com/mikestefanello/pagoda/funcmap" "github.com/mikestefanello/pagoda/funcmap"
) )
// TemplateRenderer provides a flexible and easy to use method of rendering simple templates or complex sets of type (
// templates while also providing caching and/or hot-reloading depending on your current environment // TemplateRenderer provides a flexible and easy to use method of rendering simple templates or complex sets of
type TemplateRenderer struct { // templates while also providing caching and/or hot-reloading depending on your current environment
// templateCache stores a cache of parsed page templates TemplateRenderer struct {
templateCache sync.Map // templateCache stores a cache of parsed page templates
templateCache sync.Map
// funcMap stores the template function map // funcMap stores the template function map
funcMap template.FuncMap funcMap template.FuncMap
// templatePath stores the complete path to the templates directory // templatePath stores the complete path to the templates directory
templatesPath string templatesPath string
// config stores application configuration // config stores application configuration
config *config.Config 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 // NewTemplateRenderer creates a new TemplateRenderer
func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer { func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer {
@ -47,123 +73,12 @@ func NewTemplateRenderer(cfg *config.Config) *TemplateRenderer {
return t return t
} }
// Parse parses a set of templates and caches them for quick execution // Parse creates a template build operation
// If the application environment is set to local, the cache will be bypassed and templates will be func (t *TemplateRenderer) Parse() *templateBuilder {
// parsed upon each request so hot-reloading is possible without restarts. return &templateBuilder{
// renderer: t,
// All template files and template directories must be provided relative to the templates directory build: &templateBuild{},
// 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)
}
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 // 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 // getCacheKey gets a cache key for a given group and ID
func (t *TemplateRenderer) getCacheKey(cacheGroup, cacheID string) string { func (t *TemplateRenderer) getCacheKey(group, key string) string {
return fmt.Sprintf("%s:%s", cacheGroup, cacheID) 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)
} }

View file

@ -19,13 +19,14 @@ func TestTemplateRenderer(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
// Parse in to the cache // Parse in to the cache
err = c.TemplateRenderer.Parse( tpl, err := c.TemplateRenderer.
group, Parse().
id, Group(group).
"htmx", Key(id).
[]string{"htmx", "pages/error"}, Base("htmx").
[]string{"components"}, Files("htmx", "pages/error").
) Directories("components").
Store()
require.NoError(t, err) require.NoError(t, err)
// Should exist now // Should exist now
@ -41,7 +42,7 @@ func TestTemplateRenderer(t *testing.T) {
for _, f := range components { for _, f := range components {
expectedTemplates[f.Name()] = true expectedTemplates[f.Name()] = true
} }
for _, v := range parsed.Templates() { for _, v := range parsed.Template.Templates() {
delete(expectedTemplates, v.Name()) delete(expectedTemplates, v.Name())
} }
assert.Empty(t, expectedTemplates) assert.Empty(t, expectedTemplates)
@ -51,19 +52,20 @@ func TestTemplateRenderer(t *testing.T) {
}{ }{
StatusCode: 500, StatusCode: 500,
} }
buf, err := c.TemplateRenderer.Execute(group, id, "htmx", data) buf, err := tpl.Execute(data)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, buf) require.NotNil(t, buf)
assert.Contains(t, buf.String(), "Please try again") assert.Contains(t, buf.String(), "Please try again")
buf, err = c.TemplateRenderer.ParseAndExecute( buf, err = c.TemplateRenderer.
group, Parse().
id, Group(group).
"htmx", Key(id).
[]string{"htmx", "pages/error"}, Base("htmx").
[]string{"components"}, Files("htmx", "pages/error").
data, Directories("components").
) Execute(data)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, buf) require.NotNil(t, buf)
assert.Contains(t, buf.String(), "Please try again") assert.Contains(t, buf.String(), "Please try again")