Templates complete

drew/lets-go
Drew Bednar 3 months ago
parent f0da255c6a
commit 13445115da

@ -126,7 +126,8 @@ The [official template docs](https://pkg.go.dev/text/template#hdr-Functions) can
- If the type that youre 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 `<span>{{.Snippet.Created.Weekday}}</span>`. You can also pass parameters `<span>{{.Snippet.Created.AddDate 0 6 0}}</span>`
- 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

@ -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!")

@ -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

@ -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())

@ -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
}

@ -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 thats 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)
}

@ -17,7 +17,7 @@
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
<footer>Powered by <a href='https://golang.org/'>Go</a> in {{ .CurrentYear }}</footer>
<script src='/static/js/main.js' type='text/javascript'></script>
</body>
</html>

@ -1,6 +1,23 @@
{{define "title"}}Home{{end}}
{{define "main" -}}
<h2>Latest Snippets</h2>
<p>There's nothing to see yet!</p>
{{ if .Snippets}}
<table>
<tr>
<th>Title</th>
<th>Created</th>
<th>ID</th>
</tr>
{{range .Snippets}}
<tr>
<td><a href='/snippet/view/{{.ID}}'>{{.Title.String}}</a></td>
<!-- this is an example of pipelineing instead of function call -->
<td>{{.CreatedAt | humanDate }}</td>
<td>#{{.ID}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>There's nothing to see yet!</p>
{{end}}
{{- end -}}

@ -1,17 +1,19 @@
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
{{ with .Snippet }}
<!-- This is a comment that will be stripped out by html/template -->
<div class="snippet">
<div class="metadata">
<strong>{{.Snippet.Title.String }}</strong>
<span>#{{.Snippet.ID}}</span>
<strong>{{.Title.String }}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Snippet.Content.String }}</code></pre>
<pre><code>{{.Content.String }}</code></pre>
<div class="metadata">
<time>Created: {{.Snippet.CreatedAt}}</time>
<time>Expires: {{.Snippet.ExpiresAt}}</time>
<time>Created: {{ humanDate .CreatedAt}}</time>
<time>Expires: {{ humanDate .ExpiresAt}}</time>
</div>
</div>
{{ end }}
{{end}}
Loading…
Cancel
Save