diff --git a/README.md b/README.md
index e058b5b..8fd3129 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,8 @@ The [official template docs](https://pkg.go.dev/text/template#hdr-Functions) can
- If the type that you’re yielding between `{{ }}` tags has methods defined against it, you can call these methods (so long as they are exported and they return only a single value — or a single value and an error). Example `.Snippet.Created` struct field has the underlying type `time.Time` so `{{.Snippet.Created.Weekday}}`. You can also pass parameters `{{.Snippet.Created.AddDate 0 6 0}}`
- The `html/template` package always strips out any HTML comments you include in your templates which also help avoid XSS attacks when rendering dynamic content.
- Variable names must be prefixed by a dollar sign and can contain alphanumeric characters only. Ex: `{{$bar := len .Foo}}`
-
+- You can combine multiple template functions with `()`. Ex: `{{if (gt (len .Foo) 99)}} C1 {{end}}`
+- Within a `{{range}}` action you can use the `{{break}}` command to end the loop early, and `{{continue}}` to immediately start the next loop iteration.
### Template Actions
diff --git a/cmd/ratchetd/main.go b/cmd/ratchetd/main.go
index d9ccf1b..524e2d9 100644
--- a/cmd/ratchetd/main.go
+++ b/cmd/ratchetd/main.go
@@ -42,10 +42,12 @@ func main() {
// Close db connection before exiting main.
defer db.Close()
+ tc, err := server.InitTemplateCache()
+
// Propagate build information to root package to share globally
// ratchet.Version = strings.TrimPrefix(version, "")
// ratchet.Commit = commit
- server := server.NewRatchetServer(logger, db)
+ server := server.NewRatchetServer(logger, tc, db)
// START SERVING REQUESTS
slog.Debug("Herp dirp!")
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index bb295b0..cceeda2 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -11,7 +11,7 @@ import (
)
// TODO function should accept and a pointer to an interface allowing for mocking in tests.
-func handleHome(logger *slog.Logger, snippetService *model.SnippetService) http.Handler {
+func handleHome(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
logger.Info("request received", "method", "GET", "path", "/")
@@ -26,9 +26,15 @@ func handleHome(logger *slog.Logger, snippetService *model.SnippetService) http.
logger.Debug(fmt.Sprintf("%d snippets retrieved", len(snippets)))
- for _, snippet := range snippets {
- fmt.Fprintf(w, "%+v\n", snippet)
- }
+ // Old way. We want default data so
+ // data := templateData{
+ // Snippets: snippets,
+ // }
+ data := newTemplateData()
+ data.Snippets = snippets
+
+ renderTemplate(w, r, tc, http.StatusOK, "home.go.tmpl", data)
+
// // Initialize a slice containing the paths to the two files. It's important
// // to note that the file containing our base template must be the *first*
// // file in the slice.
@@ -38,24 +44,25 @@ func handleHome(logger *slog.Logger, snippetService *model.SnippetService) http.
// "./ui/html/pages/home.go.tmpl",
// }
- // // read template file into template set.
+ // read template file into template set.
// ts, err := template.ParseFiles(files...)
+ // ts, err := parseTemplateFiles(files...)
// if err != nil {
- // rs.serverError(w, r, err)
+ // serverError(w, r, err)
// return
// }
// // Write template content to response body
- // err = ts.ExecuteTemplate(w, "base", nil)
+ // err = ts.ExecuteTemplate(w, "base", data)
// if err != nil {
// // This is the older more verbose way of doing what RatchetServer.serverError does
// // rs.logger.Error(err.Error())
// // http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- // rs.serverError(w, r, err)
+ // serverError(w, r, err)
// }
})
}
-func handleSnippetView(logger *slog.Logger, snippetService *model.SnippetService) http.Handler {
+func handleSnippetView(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
@@ -78,24 +85,24 @@ func handleSnippetView(logger *slog.Logger, snippetService *model.SnippetService
return
}
- files := []string{
- "./ui/html/base.go.tmpl",
- "./ui/html/partials/nav.go.tmpl",
- "./ui/html/pages/view.go.tmpl",
- }
+ // files := []string{
+ // "./ui/html/base.go.tmpl",
+ // "./ui/html/partials/nav.go.tmpl",
+ // "./ui/html/pages/view.go.tmpl",
+ // }
- //ts, err := template.ParseFiles(files...)
- ts, err := parseTemplateFiles(files...)
- if err != nil {
- serverError(w, r, err)
- return
- }
+ // //ts, err := template.ParseFiles(files...)
+ // ts, err := parseTemplateFiles(files...)
+ // if err != nil {
+ // serverError(w, r, err)
+ // return
+ // }
- data := templateData{
- Snippet: snippet,
- }
+ // data := templateData{
+ // Snippet: snippet,
+ // }
- logger.Debug(fmt.Sprintf("created template: %s", ts.Name()))
+ // logger.Debug(fmt.Sprintf("created template: %s", ts.Name()))
// Any data that you pass as the final parameter to ts.ExecuteTemplate()
// is represented within your HTML templates by the . character (referred to as dot).
@@ -103,10 +110,16 @@ func handleSnippetView(logger *slog.Logger, snippetService *model.SnippetService
// When the underlying type of dot is a struct, you can render (or yield) the value
// of any exported field in your templates by postfixing dot with the field name
// field, we could yield the snippet title by writing {{.Title}} in our templates.
- err = ts.ExecuteTemplate(w, "base", data)
- if err != nil {
- serverError(w, r, err)
- }
+ // err = ts.ExecuteTemplate(w, "base", data)
+ // if err != nil {
+ // serverError(w, r, err)
+ // }
+ // data := templateData{
+ // Snippet: snippet,
+ // }
+ data := newTemplateData()
+ data.Snippet = snippet
+ renderTemplate(w, r, tc, http.StatusOK, "view.go.tmpl", data)
})
}
@@ -119,7 +132,7 @@ func handleSnippetCreateGet() http.Handler {
// snippetCreate handles display of the form used to create snippets
//
// curl -iL -d "" http://localhost:5001/snippet/create
-func handleSnippetCreatePost(logger *slog.Logger, snippetService *model.SnippetService) http.Handler {
+func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// example of a custom header. Must be done before calling WriteHeader
diff --git a/internal/server/routes.go b/internal/server/routes.go
index b472bf1..5563ebe 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -10,6 +10,7 @@ import (
func addRoutes(mux *http.ServeMux,
logger *slog.Logger,
+ tc *TemplateCache,
db *sql.DB,
snippetService *model.SnippetService) http.Handler {
@@ -17,10 +18,10 @@ func addRoutes(mux *http.ServeMux,
// resulting in this route requiring an exact match on "/" only
// You can only include one HTTP method in a route pattern if you choose
// GET will match GET & HEAD http request methods
- mux.Handle("GET /{$}", handleHome(logger, snippetService))
- mux.Handle("GET /snippet/view/{id}", handleSnippetView(logger, snippetService))
+ mux.Handle("GET /{$}", handleHome(logger, tc, snippetService))
+ mux.Handle("GET /snippet/view/{id}", handleSnippetView(logger, tc, snippetService))
mux.Handle("GET /snippet/create", handleSnippetCreateGet())
- mux.Handle("POST /snippet/create", handleSnippetCreatePost(logger, snippetService))
+ mux.Handle("POST /snippet/create", handleSnippetCreatePost(logger, tc, snippetService))
// mux.Handle("/something", handleSomething(logger, config))
// mux.Handle("/healthz", handleHealthzPlease(logger))
// mux.Handle("/", http.NotFoundHandler())
diff --git a/internal/server/server.go b/internal/server/server.go
index 2f5f312..9f1ac50 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -11,16 +11,19 @@ import (
type RatchetServer struct {
http.Handler
- logger *slog.Logger
+ logger *slog.Logger
+ templateCache *TemplateCache
//Services used by HTTP routes
snippetService *model.SnippetService
UserService model.UserService
}
-func NewRatchetServer(logger *slog.Logger, db *sql.DB) *RatchetServer {
+func NewRatchetServer(logger *slog.Logger, tc *TemplateCache, db *sql.DB) *RatchetServer {
rs := new(RatchetServer)
rs.logger = logger
rs.snippetService = &model.SnippetService{DB: db}
+
+ rs.templateCache = tc
// TODO implement middleware that disables directory listings
fileServer := http.FileServer(http.Dir("./ui/static/"))
router := http.NewServeMux()
@@ -42,6 +45,6 @@ func NewRatchetServer(logger *slog.Logger, db *sql.DB) *RatchetServer {
// Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver.
// SEE we can really clean things up by moving this into routes.go and handlers.go
- rs.Handler = addRoutes(router, rs.logger, db, rs.snippetService)
+ rs.Handler = addRoutes(router, rs.logger, rs.templateCache, db, rs.snippetService)
return rs
}
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 193cd42..2909eaa 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -1,8 +1,13 @@
package server
import (
+ "bytes"
"database/sql"
+ "fmt"
"html/template"
+ "net/http"
+ "path/filepath"
+ "time"
"git.runcible.io/learning/ratchet/internal/model"
)
@@ -12,7 +17,25 @@ import (
// At the moment it only contains one field, but we'll add more
// to it as the build progresses.
type templateData struct {
- Snippet model.Snippet
+ CurrentYear int
+ Snippet model.Snippet
+ Snippets []model.Snippet
+}
+
+// newTemplateData is useful to inject default values. Example CSRF tokens for forms.
+func newTemplateData() templateData {
+ return templateData{CurrentYear: time.Now().Year()}
+}
+
+// TEMPLATE FUNCTIONS
+
+// Custom template functions can accept as many parameters as they need to, but they must return one value only.
+// The only exception to this is if you want to return an error as the second value, in which case that’s OK too.
+
+// Create a humanDate function which returns a nicely formatted string
+// representation of a time.Time object.
+func humanDate(t time.Time) string {
+ return t.Format("02 Jan 2006 at 15:04")
}
// TEMPLATE FILTERS
@@ -25,6 +48,7 @@ var templateFuncMap = template.FuncMap{
}
return ""
},
+ "humanDate": humanDate,
}
// parseTemplateFiles parses the provided template files and extends the resulting
@@ -48,3 +72,67 @@ func parseTemplateFiles(files ...string) (*template.Template, error) {
}
return tmpl, nil
}
+
+// TODO use an go:embed FS instead of file paths
+
+type TemplateCache map[string]*template.Template
+
+func InitTemplateCache() (*TemplateCache, error) {
+ cache := TemplateCache{}
+
+ pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
+ if err != nil {
+ return nil, err
+ }
+
+ for _, page := range pages {
+ name := filepath.Base(page)
+
+ tmpl := template.New(name).Funcs(templateFuncMap)
+ // Parse the base template file into a template set.
+ tmpl, err = tmpl.ParseFiles("./ui/html/base.go.tmpl")
+ if err != nil {
+ return nil, err
+ }
+ // Call ParseGlob() *on this template set* to add any partials.
+ tmpl, err = tmpl.ParseGlob("./ui/html/partials/*.tmpl")
+ if err != nil {
+ return nil, err
+ }
+
+ // Call ParseFiles() *on this template set* to add the page template.
+ tmpl, err = tmpl.ParseFiles(page)
+ if err != nil {
+ return nil, err
+ }
+
+ cache[name] = tmpl
+
+ }
+ return &cache, nil
+}
+
+func renderTemplate(w http.ResponseWriter, r *http.Request, tc *TemplateCache, status int, page string, data templateData) {
+ cache := *tc
+ ts, ok := cache[page]
+ if !ok {
+ err := fmt.Errorf("the template %s does not exist", page)
+ serverError(w, r, err)
+ return
+ }
+
+ // Write the template results to a buffer to capture templating errors before writing
+ // to ResponseWriter
+ buf := new(bytes.Buffer)
+
+ err := ts.ExecuteTemplate(buf, "base", data)
+ if err != nil {
+ serverError(w, r, err)
+ return
+ }
+
+ w.WriteHeader(status)
+
+ buf.WriteTo(w)
+
+}
diff --git a/ui/html/base.go.tmpl b/ui/html/base.go.tmpl
index 09d86e4..d9f03b1 100644
--- a/ui/html/base.go.tmpl
+++ b/ui/html/base.go.tmpl
@@ -17,7 +17,7 @@
{{template "main" .}}
-
+