4. Templates HTML dinámicos

En este post vamos a centrarnos en mostrar datos dinámicos desde nuestro MySQL en nuestras paginas HTML.

Lo que aprenderemos:

  • Pasar datos dinámicos a nuestros templantes HTML de una manera simple y segura.
  • Usar varias acciones y funciones que nos otorga el paquete html/template para controlar como se muestran los datos dinámicos.
  • Crear un template cache para que nuestros templates no tengan que ser leídos desde el disco en cada petición HTTP.
  • Manejar de una manera elegante template rendering errors en tiempo de ejecución.
  • Implementar un patrón para pasar common dynamic data de las paginas web sin repetir código.
  • Crear nuestros propias funciones personalizadas para formatear y mostrar los datos en nuestros templates HTML.

4.1 Mostrando datos dinámicos.

Actualmente nuestro handler snippetView busca los objetos models.Snippet desde la BD y luego devuelve el contenido en texto plano como respuesta HTTP.

En esta sección lo actualizaremos para que esos datos sean mostrados en el HTML.

Comencemos con el hadler snippetView y agreguemos algo de código para renderizar un nuevo template view.html .

package main
import (
    "errors"
    "fmt"
+    "html/template"
    "net/http"
    "strconv"
    "snippetbox.alexedwards.net/internal/models"
)
...
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
    	app.notFound(w)
    	return
    }
    snippet, err := app.snippets.Get(id)
    if err != nil {
    	if errors.Is(err, models.ErrNoRecord) {
    	app.notFound(w)
    } else {
        app.serverError(w, err)
    }
    return
    }
    // Initialize a slice containing the paths to the view.tmpl file,
    // plus the base layout and navigation partial that we made earlier.
    files := []string{
        "./ui/html/base.html",
        "./ui/html/partials/nav.html",
        "./ui/html/pages/view.html",
   }
   
    // Parse the template files...
    ts, err := template.ParseFiles(files...)
    if err != nil {
    	app.serverError(w, err)
    	return
    }
    // And then execute them. Notice how we are passing in the snippet
    // data (a models.Snippet struct) as the final parameter?
    err = ts.ExecuteTemplate(w, "base", snippet)
    if err != nil {
    	app.serverError(w, err)
    }
}
...

Lo siguiente es crear view.html pero antes hay un poco de teoría que hay que explicar.

Dentro de nuestro template HTML, cualquier dato dinámico que pases en representado por el caracter . (punto).

En este caso especifico, lo que esta por debajo del punto sera la estructura models.Snippet por lo que, como la estructura models.Snippet tiene Title vas a poder renderizar ese valor así {{.Title}} en el template.

Creemos ui/html/pages/view.html y agreguemos lo siguiente:

$ touch ui/html/pages/view.tmpl
{{define "title"}}Snippet #{{.ID}}{{end}}
{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Title}}</strong>
            <span>#{{.ID}}</span>
        </div>
        <pre><code>{{.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Created}}</time>
            <time>Expires: {{.Expires}}</time>
        </div>
    </div>
{{end}}

Si reiniciamos el servidor y vamos a http://localhost:4000/snippet/view?id=1 deberíamos poder ver el resultado.

4.1 Renderizando múltiples piezas de datos

Una cosa importante es que html/template permite solo pasar un item con datos dinámicos cuando renderizamos el template. Pero en el mundo real a menudo necesitamos renderizar varios de estos items con datos dinámicos en la misma pagina .

Una manera liviana y segura de hacer esto es envolver todos tus datos dinamicos en una estructura, la cual actúa como una única estrucutra para todos los datos.

Creemos en cmd/web/templates.go que contenga la estructura_templateData_ .

$ touch cmd/web/templates.go
package main
import "snippetbox.alexedwards.net/internal/models"
// 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 models.Snippet
}

Y ahora actualizamos nuestro handler snippetView para usar esta nueva estructura en nuestros templates.

package main
...
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
    app.notFound(w)
    	return
    }
    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
        app.notFound(w)
    } else {
    	app.serverError(w, err)
    }
    return
    }
    files := []string{
        "./ui/html/base.html",
        "./ui/html/partials/nav.html",
        "./ui/html/pages/view.html",
    }
    ts, err := template.ParseFiles(files...)
    if err != nil {
    	app.serverError(w, err)
    	return
    }
    // Create an instance of a templateData struct holding the snippet data.
+    data := &templateData{
+    	Snippet: snippet,
+    }
    // Pass in the templateData struct when executing the template.
+    err = ts.ExecuteTemplate(w, "base", data)
    if err != nil {
    	app.serverError(w, err)
    }
}
...

Ahora nuestros datos de los snippets esta contenidos en la estructura models.Snippet dentro de la estructura templateData. Para acceder a los datos tenemos que encadenar los campos de manera correcta, asi:

{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Snippet.Title}}</strong>
            <span>#{{.Snippet.ID}}</span>
        </div>
        <pre><code>{{.Snippet.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Snippet.Created}}</time>
            <time>Expires: {{.Snippet.Expires}}</time>
        </div>
    </div>
{{end}}

Reinicia el servidor y volve a http://localhost:4000/snippet/view?id=1 , todo debería seguir funcionando.

4.1 Información adicional

4.1 Dynamic content escaping

El paquete html/template automáticamente escapa cualquier dato que se encuentre entre {{}} , este comportamiento es muy útil para evitar ataques cross-site scripting (XSS) y esa es la razón por la que deberías usar el paquete html/template en vez de usar algo mas genérico como text/template que Go también posee.

Como ejemplo podemos ver el siguiente código:

<span>{{"<script>alert('xss attack')</script>"}}</span>

Deberías poder renderizarlo de manera segura y se veria así:

<span>&lt;script&gt;alert(&#39;xss attack&#39;)&lt;/script&gt;</span>
4.1 Llamada a métodos

Si el tipo de dato que estas llamando dentro de {{}} es un método, podrás hacerlo sin problemas ( siempre y cuando se exporten y devuelvan un valor único o un error).

Por ejemplo .Snippet.Created tiene el método Weekday() podrás ejecutarlo así:

<span>{{.Snippet.Created.Weekday}}</span>

Tambien podes pasar parametros usanod espacios asi:

<span>{{.Snippet.Created.AddDate 0 6 0}}</span>
4.1 Comentarios HTML

El paquete html/template elimina cualquier comentario que incluyas en tus plantillas html para evitar ataques XSS

4.2 Template actions and functions

En esta sección veremos las acciones y las funciones que Go nos provee.

Ya hemos hablado sobre algunas acciones ({{define}},{{template}} y {{block}}) pero tenemos tres mas las cuales nos ayudan a controlar los datos dinámicos. ({{if}},{{with}},{{range}}).

AccionDescripcion
{{if .Foo}} C1 {{else}} C2 {{end}}Si .Foo no esta vacio entonces render C1 sino C2
{{with .Foo}} C1 {{else}} C2 {{end}}Si .Foo no esta vacio entonces setea el punto con el valor de .Foo y renderiza C1 sino C2.
{{range .Foo}} C1 {{else}} C2 {{end}}Si el length de .Foo es mayor a cero entonces haz un loop sobre cada elemento, setea el unto con el valor de cada elemento y renderiza el contenido C1 Si el length de .Foo es cero entonces renderiza C2. El tipo de dato de .Foo tiene que ser array,slice,map o channel.

Hay algunas cosas mas que nombre aserca de esas acciones.

  • En todas {{else}} es opcional , podes poner {{if .Foo}} C1 {{end}} si es que no tenes un c2 que quieras mostrar.
  • Los empty value son : false,0,cualquier puntero nil o interface y cualquier array,slice,map o string con length cero.
  • Es importante entender que with y range cambian el valor del punto. Una vez empiezas a usarlo lo que el punto representa puede ser diferente dependiendo de donde estas en el template y que estas haciendo.

El paquete html/template tambien provee algunas funciones las cuales pueden ser usadas para agregar lógica extra a tus templates y controlar que se renderiza en tiempo de ejecución. Podes encontrar una lista completa de funciones aca , pero los mas importantes son:

FunctionDescription
{{eq .Foo .Bar}}true if .Foo es igual to .Bar
{{ne .Foo .Bar}}true if .Foo no es igual to .Bar
{{not .Foo}}Regresa un booleano con la negación de Foo
{{or .Foo .Bar}}Regresa .Foo si.Foo no es vacio; sino regresa .Bar
{{index .Foo i}}Regresa i como indice de .Foo , Foo tiene que ser map,slice o array , i sera de tipo integer.
{{printf "%s-%s" .Foo .Bar}}Regresa un string formateado como lo hace fmt.Sprintf()
{{len .Foo}}Regresa el length de Foo como integer
{{$bar := len .Foo}}Asigna el length de .Foo al template de variable $bar

4.2 Usando la acción with

Una buena aportunidad para usar {{with}} es en view.html :

{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
    {{with .Snippet}}
        <div class='snippet'>
            <div class='metadata'>
                <strong>{{.Title}}</strong>
                <span>#{{.ID}}</span>
            </div>
            <pre><code>{{.Content}}</code></pre>
            <div class='metadata'>
                <time>Created: {{.Created}}</time>
                <time>Expires: {{.Expires}}</time>
            </div>
        </div>
    {{end}}
{{end}}

Ahora entre {{with .Snippet}} y el correpondiente {{end}} el valor del punto es seteado como .Snippet ,Esencialmente el punto se convierte en la estructura de models.Snippet en vez de la estructura padre templateData.

4.2 Usando las acciones if y range

Usaremos estas acciones para mostrar los últimos 10 post en la home.

Primero tenemos que actualizar la estructura templateData para que contenga el campo Snippets que contiene un slice de snippets así :

package main
import "snippetbox.alexedwards.net/internal/models"
// Include a Snippets field in the templateData struct.
type templateData struct {
    Snippet  models.Snippet
    Snippets []models.Snippet
}

Después actualizamos el handler de home para que busque los últimos snippets desde nuestra DB y lo pasamos al template home.html

package main
...
func (app *application) home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        app.notFound(w)
        return
    }
    snippets, err := app.snippets.Latest()
    if err != nil {
        app.serverError(w, err)
        return
    }
    files := []string{
        "./ui/html/base.html",
        "./ui/html/partials/nav.html",
        "./ui/html/pages/home.html",
    }
    ts, err := template.ParseFiles(files...)
    if err != nil {
    	app.serverError(w, err)
    	return
    }
    // Create an instance of a templateData struct holding the slice of
    // snippets.
    data := &templateData{
    	Snippets: snippets,
    }
    // Pass in the templateData struct when executing the template.
    err = ts.ExecuteTemplate(w, "base", data)
    if err != nil {
    	app.serverError(w, err)
    }
}
...

Ahora actualicemos ui/html/pages/home.html haciendo uso de {{if}} y {{range}} para mostrar una tabla.

  • Nosotros queremos usar {{if}} para revisar si el slice con los snippet esta vacio o no. Si esta vacio queremos mostrar There’s nothing to see here yet! de otra manera mostraremos los snippet.
  • Queremos usar {{range}} para iterar sobre todos los snippets in el slice.
{{define "title"}}Home{{end}}
{{define "main"}}
    <h2>Latest Snippets</h2>
    {{if .Snippets}}
    <table>
        <tr>
            <th>Title</th>
            <th>Created</th>
            <th>ID</th>
        </tr>
        {{range .Snippets}}
        <tr>
            <td><a href='/snippet/view?id={{.ID}}'>{{.Title}}</a></td>
            <td>{{.Created}}</td>
            <td>#{{.ID}}</td>
        </tr>
        {{end}}
    </table>
    {{else}}
    <p>There's nothing to see here... yet!</p>
    {{end}}
{{end}}

Reniciamos el servidor y vamos a http://localhost:4000 deberías poder ver la tabla.

4.2 Información adicional

4.2 Combinación de funciones

Es posible combinar varias funciones en nuestro templates, usando paréntesis () para envolver sus funciones y sus argumentos en la medida que lo necesitemos.

Por ejemplo, el siguiente tag renderizara el contenido c1 si el length de Foo es mayor a 99

{{if (gt (len .Foo) 99)}} C1 {{end}}
4.2 Controlando el comportamiento del loop

Dentro de la accion {{range}} podes usar {{break}} para teminar un loop de manera temprana, y usar {{continue}} para comenzar inmediatamente el siguiente loop.

{{range .Foo}}
    // Skip this iteration if the .ID value equals 99.
    {{if eq .ID 99}}
    	{{continue}}
    {{end}}
    // ...
{{end}}
{{range .Foo}}
    // Skip this iteration if the .ID value equals 99.
    {{if eq .ID 99}}
    	{{break}}
    {{end}}
    // ...
{{end}}

4.3 Caching templates

Antes de agregar mas funcionalidad a nuestros templates HTML , es bueno hacer algunas optimizaciones a nuestro código. Tenemos dos principales problemas:

  • Cada vez que renderizamos la pagina, nuestra aplicacion lee y parsea los templates relevantes usando template.ParseFiles() . Podríamos evitar este trabajo duplicado pasando los archivos solo una vez ( cuando inicia nuestra app) y almacenando los templates parseados en la memoria cache.
  • Tenemos código duplicado en los handlers home y snippetView, podemos evitar esta duplicidad creando un helper.

Hagamos el primer punto y creemos un in-memory map que sea de tipo map[string]*template.Template para cachear los template parseados. Vamos a cmd/web/templates.go y agreguemos el siguiente código.

package main
import (
"html/template" // New import
"path/filepath" // New import
"github.com/nahueldev23/internal/models"
)
...
func newTemplateCache() (map[string]*template.Template, error) {
// Initialize a new map to act as the cache.
cache := map[string]*template.Template{}
// Use the filepath.Glob() function to get a slice of all filepaths that
// match the pattern "./ui/html/pages/*.tmpl". This will essentially gives
// us a slice of all the filepaths for our application 'page' templates
// like: [ui/html/pages/home.html ui/html/pages/view.html]
pages, err := filepath.Glob("./ui/html/pages/*.html")
if err != nil {
return nil, err
}
// Loop through the page filepaths one-by-one.
for _, page := range pages {
// Extract the file name (like 'home.tmpl') from the full filepath
// and assign it to the name variable.
name := filepath.Base(page)
// Create a slice containing the filepaths for our base template, any
// partials and the page.
files := []string{
"./ui/html/base.html",
"./ui/html/partials/nav.html",
page,
}
// Parse the files into a template set.
ts, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
// Add the template set to the map, using the name of the page
// (like 'home.tmpl') as the key.
cache[name] = ts
}
// Return the map.
return cache, nil
}

El siguiente paso es inicializar el cache en main() y hacerlo visible en nuestros handlers como dependecia a través de la estructura application asi:

import (
"database/sql"
"flag"
+"html/template" // New import
"log"
"net/http"
"os"
"snippetbox.alexedwards.net/internal/models"
_ "github.com/go-sql-driver/mysql"
)
// Add a templateCache field to the application struct.
type application struct {
        errorLog*log.Logger
        infoLog*log.Logger
        snippets*models.SnippetModel
+        templateCache map[string]*template.Template
    }
    func main() {
        addr := flag.String("addr", ":4000", "HTTP network address")
        dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
        flag.Parse()
        infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
        errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
        db, err := openDB(*dsn)
        if err != nil {
        	errorLog.Fatal(err)
        }
        defer db.Close()
        // Initialize a new template cache...
+       templateCache, err := newTemplateCache()
+       if err != nil {
+       	errorLog.Fatal(err)
+       }
        // And add it to the application dependencies.
        app := &application{
            errorLog:errorLog,
            infoLog:infoLog,
            snippets:&models.SnippetModel{DB: db},
+            templateCache: templateCache,
        }
        srv := &http.Server{
            Addr:*addr,
            ErrorLog: errorLog,
            Handler:
            app.routes(),
        }
        infoLog.Printf("Starting server on %s", *addr)
        err = srv.ListenAndServe()
        errorLog.Fatal(err)
}
...

En este punto tenemos en cache los templates relevantes de cada una de nuestras paginas y nuestros handlers tienen acceso a este cache a través de la estructura application.

Vamos a solucionar el segundo problema, el codigo duplicado. Crearemos un helper de manera que sea fácil renderizar templates desde el cache.

Vamos a cmd/web/helpers.go y agreguemos el metodo render()

package main
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
    // Retrieve the appropriate template set from the cache based on the page
    // name (like 'home.html'). If no entry exists in the cache with the
    // provided name, then create a new error and call the serverError() helper
    // method that we made earlier and return.
    ts, ok := app.templateCache[page]
    if !ok {
        err := fmt.Errorf("the template %s does not exist", page)
        app.serverError(w, err)
        return
    }
    // Write out the provided HTTP status code ('200 OK', '400 Bad Request'
    // etc).
    w.WriteHeader(status)
    // Execute the template set and write the response body. Again, if there
    // is any error we call the the serverError() helper.
    err := ts.ExecuteTemplate(w, "base", data)
    if err != nil {
    	app.serverError(w, err)
    }
}

Una vez hecho esto podremos ver los beneficios y como simplifica a nuestros handlers:

package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/nahueldev23/internal/models"
)
func (app *application) home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        app.notFound(w)
        return
    }
    snippets, err := app.snippets.Latest()
    if err != nil {
        app.serverError(w, err)
        return
    }
-  //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.
-
-	files := []string{
-		"./ui/html/base.html",
-		"./ui/html/partials/nav.html",
-		"./ui/html/pages/home.html",
-	}
-
-	// Use the template.ParseFiles() function to read the files and store the
-	// templates in a template set. Notice that we can pass the slice of file
-	// paths as a variadic parameter?
-	ts, err := template.ParseFiles(files...)
-	if err != nil {
-		app.serverError(w, err)
-		return
-	}
-	data := &templateData{
-		Snippets: snippets,
-	}
-	// Use the ExecuteTemplate() method to write the content of the "base"
-	// template as the response body.
-
-	err = ts.ExecuteTemplate(w, "base", data)
-	if err != nil {
-		app.serverError(w, err)
-	}
    // Use the new render helper.
+    app.render(w, http.StatusOK, "home.html", &templateData{Snippets: snippets,})}

    func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
        app.notFound(w)
        return
    }
    snippet, err := app.snippets.Get(id)
    if err != nil {
      if errors.Is(err, models.ErrNoRecord) {
          app.notFound(w)
      } else {
      	app.serverError(w, err)
      }
    return
    }
-    // Initialize a slice containing the paths to the view.tmpl file,
-	// plus the base layout and navigation partial that we made earlier.
-	files := []string{
-		"./ui/html/base.html",
-		"./ui/html/partials/nav.html",
-		"./ui/html/pages/view.html",
-	}
-
-	// Parse the template files...
-	ts, err := template.ParseFiles(files...)
-	if err != nil {
-		app.serverError(w, err)
-		return
-	}
-	data := &templateData{
-		Snippet: snippet,
-	}
-	// And then execute them. Notice how we are passing in the snippet
-	// data (a models.Snippet struct) as the final parameter?
-	err = ts.ExecuteTemplate(w, "base", data)
-	if err != nil {
-		app.serverError(w, err)
-	}
    // Use the new render helper.
+    app.render(w, http.StatusOK, "view.html", &templateData{
    Snippet: snippet,
    })
}
...

Si reiniciamos el servidor veremos que todo sigue funcionando como antes.

Parsear partials automaticamente

Antes de continuar vamos a hacer a nuestra función newTemplateCache() un poco mas flexible para que automáticamente parsee todos nuestros templates que estén en la carpeta ui/html/partials ademas de nav.html

Esto nos ahorrara tiempo y potenciales bugs cuando queramos agregar partials adicionales.Vamos a cmd/web/templates.go

func newTemplateCache() (map[string]*template.Template, error) {
    cache := map[string]*template.Template{}
    pages, err := filepath.Glob("./ui/html/pages/*.html")
    if err != nil {
    	return nil, err
    }
    for _, page := range pages {
        name := filepath.Base(page)
-        // Create a slice containing the filepaths for our base template, any
-        // partials and the page.
-        files := []string{
-        "./ui/html/base.html",
-        "./ui/html/partials/nav.html",
-        page,
-        }
-        // Parse the files into a template set.
-		ts, err := template.ParseFiles(files...)
        
+        // Parse the base template file into a template set.
+        ts, err := template.ParseFiles("./ui/html/base.html")
+        if err != nil {
+        	return nil, err
+    	}
        
+       // Call ParseGlob() *on this template set* to add any partials.
+       ts, err = ts.ParseGlob("./ui/html/partials/*.html")
+       if err != nil {
+            return nil, err
+        }
        
        // Add the template set to the map as normal...
        cache[name] = ts
        }
        return cache, nil
}	

4.4 Capturando runtime errors

Tan pronto como comencemos a agregar comportamiento dinamico a nuestros templates HTML , corremos el riesgo de encontrarnos con runtime errors.

Provoquemos un error a ver que pasa en view.html

{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
{{with .Snippet}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Title}}</strong>
            <span>#{{.ID}}</span>
        </div>
        {{len nil}} <!-- Deliberate error -->
        <pre><code>{{.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Created}}</time>
            <time>Expires: {{.Expires}}</time>
        </div>
    </div>
    {{end}}
{{end}}

{{len nil}} arrojara error porque en Go nil no tiene length.

Reincia el servidor y haz una petición a view mediante curl

$ curl -i "http://localhost:4000/snippet/view?id=1"
HTTP/1.1 200 OK
Date: Tue, 01 Feb 2022 09:20:49 GMT
Content-Length: 762
Content-Type: text/html; charset=utf-8
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Snippet #1 - Snippetbox</title>
<link rel='stylesheet' href='/static/css/main.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<nav>
<a href='/'>Home</a>
</nav>
<main>
<div class='snippet'>
<div class='metadata'>
<strong>An old silent pond</strong>
<span>#1</span>
</div>
Internal Server Error

Esto esta mal porque nuestra app arrojo un erro,pero respondio con un status 200 OK y peor aun , devolvio un HTML por la mitad.

Para arreglar esto necesitamos hacer que la plantilla haga un proceso de dos etapas. Primero deberiamos hacer un trial render , escribiendo el template en un buffer. Si falla , podemos responder al usuario con un mensaje de error. Pero si funciona podemos escribir el contenido en el buffer con http.ResponseWritter.

Actualicemos render() con este enfoque.

package main
import (
"bytes" // New import
"fmt"
"net/http"
"runtime/debug"
)
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
    ts, ok := app.templateCache[page]
    if !ok {
        err := fmt.Errorf("the template %s does not exist", page)
        app.serverError(w, err)
        return
    }
    // Initialize a new buffer.
    buf := new(bytes.Buffer)
    // Write the template to the buffer, instead of straight to the
    // http.ResponseWriter. If there's an error, call our serverError() helper
    // and then return.
    err := ts.ExecuteTemplate(buf, "base", data)
    if err != nil {
        app.serverError(w, err)
        return
    }
    // If the template is written to the buffer without any errors, we are safe
    // to go ahead and write the HTTP status code to http.ResponseWriter.
    w.WriteHeader(status)
    // Write the contents of the buffer to the http.ResponseWriter. Note: this
    // is another time where we pass our http.ResponseWriter to a function that
    // takes an io.Writer.
    buf.WriteTo(w)
}

Reiniciamos la app y ahora deberíamos tener el error 500 Internal Server Error

4.5 Datos dinámicos comunes.

En algunas web puede que quieras tener datos comunes en varias paginas , o en todas , por ejemplo el nombre y la foto de perfil de un usuario o un token CSFR en todas las paginas que tengan formularios.

En nuestro caso haremos algo simple, agregaremos el anio actual en el footer.

Comenzaremos agregando un nuevo campo currentYear en la estructura templateData.

package main
...
// Add a CurrentYear field to the templateData struct.
type templateData struct {
    CurrentYear int
    Snippet models.Snippet
    Snippets []models.Snippet
}
...

Lo siguiente es agregar un helper llamado newTemplateData() el cual nos retornara una estructura inicializada de templateData con el current year. cmd/web/helpers.go

package main
import (
    "bytes"
    "fmt"
    "net/http"
    "runtime/debug"
    "time" // New import
)
...
// Create an newTemplateData() helper, which returns a pointer to a templateData
// struct initialized with the current year. Note that we're not using the
// *http.Request parameter here at the moment, but we will do later in the book.
func (app *application) newTemplateData(r *http.Request) *templateData {
    return &templateData{
    	CurrentYear: time.Now().Year(),
    }
}
...

Ahora actualicemos la home en el handler snippetView para que use newTemplateData() así:

package main
...
func (app *application) home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        app.notFound(w)
        return
    }
    
    snippets, err := app.snippets.Latest()
    if err != nil {
        app.serverError(w, err)
        return
    }
+        // Call the newTemplateData() helper to get a templateData struct containing
+        // the 'default' data (which for now is just the current year), and add the
+        // snippets slice to it.
+        data := app.newTemplateData(r)
+        data.Snippets = snippets
+        // Pass the data to the render() helper as normal.
+        app.render(w, http.StatusOK, "home.html", data)
-        app.render(w, http.StatusOK, "home.html", &templateData{Snippets: snippets})
    }

    func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
        app.notFound(w)
        return
    }
    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
        	app.notFound(w)
        } else {
        	app.serverError(w, err)
        }
        return
    }
    // And do the same thing again here...
+   data := app.newTemplateData(r)
+   data.Snippet = snippet
+   app.render(w, http.StatusOK, "view.html", data)
-   app.render(w, http.StatusOK, "home.html", &templateData{Snippet: snippet})
}
...

Finalmente mostramos en ui/html/base.html la fecha en el footer así:

 {{define "base"}}
 <!DOCTYPE html>
 <html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>{{template "title" .}} - Snippetbox</title>
    <!-- Link to the CSS stylesheet and favicon -->
    <link rel="stylesheet" href="/static/css/main.css" />
    <link
      rel="shortcut icon"
      href="/static/img/favicon.ico"
      type="image/x-icon"
    />
    <!-- Also link to some fonts hosted by Google -->
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?		family=Ubuntu+Mono:400,700"
    />
  </head>

  <body>
    <header>
      <h1><a href="/">Snippetbox</a></h1>
    </header>
    {{template "nav" .}}
    <main>{{template "main" .}}</main>
    <footer>
+      Powered by <a href="https://golang.org/">Go</a> in {{.CurrentYear}}
    </footer>
    <!-- And include the JavaScript file -->
    <script src="/static/js/main.js" type="text/javascript"></script>
  </body>
</html>
{{end}}

4.6 Template functions personalizadas

En la ultima parte de esta sección voy a explicar como crear nuestras propias funciones para usar en los templates de Go.

Vamos a crear una función humanDate() la cual devuelve una fecha en un formato mas ameno a la vista , algo asi: 02 Jan 2022 at 15:20 en vez de 2022-01-02 15:04:00 +0000 UTC que es el formato por default.

Tenemos que seguir dos pasos para lograr esto:

  1. Necesitamos crear un template.FuncMap que contenga nuestra función personalizada humanDate
  2. Necesitamos usar template.Funcs para registrarlo antes de poder usarlo en los templates.

Comencemos agregando lo siguiente a templates.go

package main

import (
	"html/template"
	"path/filepath"
	"time"

	"github.com/nahueldev23/snippetbox/internal/models"
)

...

// Initialize a template.FuncMap object and store it in a global variable. This is
// essentially a string-keyed map which acts as a lookup between the names of our
// custom template functions and the functions themselves.
+ var functions = template.FuncMap{
+	"humanDate": humanDate,
+}

func newTemplateCache() (map[string]*template.Template, error) {
	// Initialize a new map to act as the cache.
	cache := map[string]*template.Template{}
	// Use the filepath.Glob() function to get a slice of all filepaths that
	// match the pattern "./ui/html/pages/*.tmpl". This will essentially gives
	// us a slice of all the filepaths for our application 'page' templates
	// like: [ui/html/pages/home.tmpl ui/html/pages/view.tmpl]
	pages, err := filepath.Glob("./ui/html/pages/*.html")
	if err != nil {
		return nil, err
	}
	// Loop through the page filepaths one-by-one.
	for _, page := range pages {
		// Extract the file name (like 'home.tmpl') from the full filepath
		// and assign it to the name variable.
		name := filepath.Base(page)

		// call the ParseFiles() method. This means we have to use template.New() to
		// create an empty template set, use the Funcs() method to register the
		// template.FuncMap, and then parse the file as normal.
+		ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.html")
        if err != nil {
			return nil, err
		}
        
-       ts, err := template.ParseFiles("./ui/html/base.html")
        
		// Call ParseGlob() *on this template set* to add any partials.
		ts, err = ts.ParseGlob("./ui/html/partials/*.html")
		if err != nil {
			return nil, err
		}

		// Call ParseFiles() *on this template set* to add the
		ts, err = ts.ParseFiles(page)
		if err != nil {
			return nil, err
		}

		// Add the template set to the map, using the name of the page
		// (like 'home.tmpl') as the key.
		cache[name] = ts
	}
	// Return the map.
	return cache, nil
}

Las funciones pueden aceptar todos los parámetros que necesites pero debe retornar solo un valor. La única excepción es cuando queremos retornar un error como segundo valor.

Usemos humanDate()

ui/html/pages/home.html

{{define "title"}}Home{{end}}
{{define "main"}}
    <h2>Latest Snippets</h2>
    {{if .Snippets}}
    <table>
        <tr>
            <th>Title</th>
            <th>Created</th>
            <th>ID</th>
        </tr>
        {{range .Snippets}}
        <tr>
            <td><a href='/snippet/view?id={{.ID}}'>{{.Title}}</a></td>
            <td>{{humanDate .Created}}</td>
            <td>#{{.ID}}</td>
        </tr>
        {{end}}
    </table>
    {{else}}
    <p>There's nothing to see here... yet!</p>
    {{end}}
{{end}}

ui/html/pages/view.html

{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}]
{{define "main"}}
{{with.Snippet}}
    <div class="snippet">
      <div class="metadata">
        <strong>{{.Title}}</strong>
        <span>#{{.ID}}</span>
      </div>
      <pre><code>{{.Content}}</code></pre>
      <div class="metadata">
        <!-- Use the new template function here -->
        <time>Created: {{humanDate .Created}}</time>
        <time>Expires: {{humanDate .Expires}}</time>
      </div>
    </div>
    {{end}} 
{{end}}

Si reinicamos el servidor podremos ver los cambios.

4.6 Información adicional

4.6 Pipelining

En el código anterior llamamos a las funciones personalizadas así:

<time>Created: {{humanDate .Created}}</time>

Una alternativa es usar | esto funciona en terminales de unix para pasar un resultado de salida como argumento de otra función. El siguIente hace lo mismo que el anterior.

<time>Created: {{.Created | humanDate}}</time>

Una característica copada del pipeline es que podes crear cadenas tan largas como quieras y los resultados de salida se iran pasando de una a otra.

<time>{{.Created | humanDate | printf "Created: %s"}}</time>