Added templates and some notes

drew/lets-go
Drew Bednar 3 months ago
parent 43b7e2d986
commit f0da255c6a

@ -89,7 +89,7 @@ Warning: http.ServeFile() does not automatically sanitize the file path. If you
## Databases
The Let's-go book calls for MySQL. We use [go-sqlite3](ggithub.com/mattn/go-sqlite3) and [go-migrate]() tool to manage migrations instead. Use `make check-system-deps` to validate all tools for this repository are installed.
The Let's-go book calls for MySQL. We use [go-sqlite3](https://github.com/mattn/go-sqlite3) and [go-migrate]() tool to manage migrations instead. Use `make check-system-deps` to validate all tools for this repository are installed.
Packages to consider adopting include sqlx or https://github.com/blockloop/scan. Both have the potential to reduce the verbosity that comes will using the standard database/sql package.
@ -115,4 +115,50 @@ To remove the package you can use `go mod tidy` if all references have been remo
```
go get github.com/foo/bar@none
```
```
## Templates
The [official template docs](https://pkg.go.dev/text/template#hdr-Functions) can be confusing to sort out. If you are new to templates check this out first: https://www.evandemond.com/kb/go-templates
- Always use `html/templates` because it escapes characters, preventing XSS attacks.
- With Nested templates you must explicitly pass or pipeline your template data. This occurs in `{{template}}` or `{{block}}` and appears like `{{template "main" .}}` or `{{block "sidebar" .}}{{end}}`
- 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}}`
### Template Actions
| Action | Description |
|-----------------------------------------------|-----------------------------------------------------------------------------------------------|
| `{{ define }}` | Defines a reusable named template. The content inside `{{ define "name" }} ... {{ end }}` can be invoked using `{{ template "name" }}`. Nested `define` calls are not allowed. |
| `{{ template }}` | Executes a template defined by `{{ define "name" }}`. Can optionally pass data into the template as `dot` (e.g., `{{ template "name" .Data }}`). |
| `{{ block }}` | Defines a named template block, similar to `define`, but allows the content to be overridden when embedding templates. Often used in base templates to create extendable sections. |
| `{{/* a comment */}}` | A comment that is ignored by the template engine. Use it to add notes or disable parts of the template without affecting the rendered output. |
### Template Functions
- For all actions below the `{{else}}` clause is optional.
- The _empty_ values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.
- the `with` and `range` actions change the value of dot. Once you start using them, what dot represents can be different depending on where you are in the template and what youre doing.
- `-}}` and `{{-` trims white space. `"{{23 -}} < {{- 45}}"` becomes `"23<45"`
| Function | Description |
|-----------------------------------------------|-------------------------------------------------------------------------------------|
| `{{ if .Foo }} C1 {{ else }} C2 {{ end }}` | If `.Foo` is not empty then render the content C1, otherwise render the content C2. |
| `{{ with .Foo }} C1 {{ else }} C2 {{ end }}` | If `.Foo` is not empty, then set `dot` to the value of `.Foo` and render the content C1, otherwise render the content C2.|
| `{{ range .Foo }} C1 {{ else }} C2 {{ end }}` | If the length of `.Foo` is greater than zero then loop over each element, setting dot to the value of each element and rendering the content C1. If the length of `.Foo` is zero then render the content C2. The underlying type of `.Foo` must be an `array`, `slice`, `map`, or `channel`.|
| `{{ define }}` | |
| `{{ define }}` | |
| Action | Description |
|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `{{ eq .Foo .Bar }}` | Yields `true` if `.Foo` is equal to `.Bar`. |
| `{{ ne .Foo .Bar }}` | Yields `true` if `.Foo` is not equal to `.Bar`. |
| `{{ not .Foo }}` | Yields the boolean negation of `.Foo`. |
| `{{ or .Foo .Bar }}` | Yields `.Foo` if `.Foo` is not empty; otherwise yields `.Bar`. |
| `{{ index .Foo i }}` | Yields the value of `.Foo` at index `i`. The underlying type of `.Foo` must be a map, slice, or array, and `i` must be an integer value. |
| `{{ printf "%s-%s" .Foo .Bar }}`| Yields a formatted string containing the `.Foo` and `.Bar` values. Works in the same way as `fmt.Sprintf()`. |
| `{{ len .Foo }}` | Yields the length of `.Foo` as an integer. |
| `{{$bar := len .Foo}}` | Assigns the length of `.Foo` to the template variable `$bar`. |

@ -78,17 +78,38 @@ func handleSnippetView(logger *slog.Logger, snippetService *model.SnippetService
return
}
// Write the snippet data as a plain-text HTTP response body.
fmt.Fprintf(w, "%+v", snippet)
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
}
data := templateData{
Snippet: snippet,
}
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).
// In this specific case, the underlying type of dot will be a models.Snippet struct.
// 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)
}
})
}
// // snippetCreate handles display of the form used to create snippets
// func (rs *RatchetServer) snippetCreate(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte("Create snippet form..."))
// }
func handleSnippetCreateGet() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Create snippet form.."))
@ -119,22 +140,3 @@ func handleSnippetCreatePost(logger *slog.Logger, snippetService *model.SnippetS
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
})
}
// func (w http.ResponseWriter, r *http.Request) {
// // example of a custom header. Must be done before calling WriteHeader
// // or they will fail to take effect.
// w.Header().Add("Server", "Dirp")
// // Create some variables holding dummy data. We'll remove these later on
// // during the build.
// title := "O snail"
// content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n Kobayashi Issa"
// expires := 7
// id, err := rs.snippetService.Insert(title, content, expires)
// if err != nil {
// rs.serverError(w, r, err)
// }
// http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
// }

@ -0,0 +1,50 @@
package server
import (
"database/sql"
"html/template"
"git.runcible.io/learning/ratchet/internal/model"
)
// Define a templateData type to act as the holding structure for
// any dynamic data that we want to pass to our HTML templates.
// 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
}
// TEMPLATE FILTERS
var templateFuncMap = template.FuncMap{
// This is a trivial example because you can simply use {{ .Content.String }} to call the sql.NullString.String() function.
"nullStringToStr": func(ns sql.NullString) string {
if ns.Valid {
return ns.String
}
return ""
},
}
// parseTemplateFiles parses the provided template files and extends the resulting
// template with additional custom template functions (filters) defined in the
// templateFuncMap.
//
// This function serves as a wrapper around template.ParseFiles, allowing the
// inclusion of reusable template functions to enhance the template's capabilities.
//
// Parameters:
// - files: A variadic list of file paths to the template files to be parsed.
//
// Returns:
// - (*template.Template): The parsed template with custom functions injected.
// - (error): An error if the template files cannot be parsed.
func parseTemplateFiles(files ...string) (*template.Template, error) {
tmpl := template.New("").Funcs(templateFuncMap)
tmpl, err := tmpl.ParseFiles(files...)
if err != nil {
return nil, err
}
return tmpl, nil
}

@ -0,0 +1,17 @@
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<!-- 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>
</div>
<pre><code>{{.Snippet.Content.String }}</code></pre>
<div class="metadata">
<time>Created: {{.Snippet.CreatedAt}}</time>
<time>Expires: {{.Snippet.ExpiresAt}}</time>
</div>
</div>
{{end}}
Loading…
Cancel
Save