Skip to content

Latest commit

 

History

History
470 lines (380 loc) · 16.3 KB

File metadata and controls

470 lines (380 loc) · 16.3 KB

7.4 Templates

What is a template?

Hopefully you're aware of the MVC (Model, View, Controller) design model, where models process data, views show the results and finally, controllers handle user requests. For views, many dynamic languages generate data by writing code in static HTML files. For instance, JSP is implemented by inserting <%=....=%>, PHP by inserting <?php.....?>, etc.

The following demonstrates the template mechanism:

Figure 7.1 Template mechanism

Most of the content that web applications respond to clients with is static, and the dynamic parts are usually very small. For example, if you need to display a list users who have visited a page, only the user name would be dynamic. The style of the list remains the same. As you can see, templates are useful for reusing static content.

Templating in Go

In Go, we have the template package to help handle templates. We can use functions like Parse, ParseFile and Execute to load templates from plain text or files, then evaluate the dynamic parts, as shown in figure 7.1.

Example:

func handler(w http.ResponseWriter, r *http.Request) {
	t := template.New("some template") // Create a template.
	t, _ = t.ParseFiles("tmpl/welcome.html", nil)  // Parse template file.
	user := GetUser() // Get current user infomration.
	t.Execute(w, user)  // merge.
}

As you can see, it's very easy to use, load and render data in templates in Go, just as in other programming languages.

For the sake of convenience, we will use the following rules in our examples:

  • Use Parse to replace ParseFiles because Parse can test content directly from strings, so we don't need any extra files.
  • Use main for every example and do not use handler.
  • Use os.Stdout to replace http.ResponseWriter since os.Stdout also implements the io.Writer interface.

Inserting data into a template

We've just shown you how to parse and render templates. Let's take it one step further and render data to our templates. Every template is an object in Go, so how do we insert fields to templates?

Fields

In Go, Every field that you intend to be rendered within a template should be put inside of {{}}. {{.}} is shorthand for the current object, which is similar to its Java or C++ counterpart. If you want to access the fields of the current object, you should use {{.FieldName}}. Notice that only exported fields can be accessed in templates. Here is an example:

package main

import (
	"html/template"
	"os"
)

type Person struct {
	UserName string
}

func main() {
	t := template.New("fieldname example")
	t, _ = t.Parse("hello {{.UserName}}!")
	p := Person{UserName: "Astaxie"}
	t.Execute(os.Stdout, p)
}

The above example outputs hello Astaxie correctly, but if we modify our struct a little bit, the following error emerges:

type Person struct {
	UserName string
	email	string  // Field is not exported.
}

t, _ = t.Parse("hello {{.UserName}}! {{.email}}")

This part of the code will not be compiled because we try to access a field that has not been exported. However, if we try to use a field that does not exist, Go simply outputs an empty string instead of an error.

If you print {{.}} in a template, Go outputs a formatted string of this object, calling fmt under the covers.

Nested fields

We know how to output a field now. What if the field is an object, and it also has its own fields? How do we print them all in one loop? We can use {{with …}}…{{end}} and {{range …}}{{end}} for exactly that purpose.

  • {% raw %}{{range}}{% endraw %} just like range in Go.
  • {% raw %}{{with}}{% endraw %} lets you write the same object name once and use . as shorthand for it ( Similar to with in VB ).

More examples:

package main

import (
	"html/template"
	"os"
)

type Friend struct {
	Fname string
}

type Person struct {
	UserName string
	Emails   []string
	Friends  []*Friend
}

func main() {
	f1 := Friend{Fname: "minux.ma"}
	f2 := Friend{Fname: "xushiwei"}
	t := template.New("fieldname example")
	t, _ = t.Parse(`hello {{.UserName}}!
			{{range .Emails}}
				an email {{.}}
			{{end}}
			{{with .Friends}}
			{{range .}}
				my friend name is {{.Fname}}
			{{end}}
			{{end}}
			`)
	p := Person{UserName: "Astaxie",
		Emails:  []string{"[email protected]", "[email protected]"},
		Friends: []*Friend{&f1, &f2}}
	t.Execute(os.Stdout, p)
}

Conditions

If you need to check for conditions in templates, you can use the if-else syntax just like you do in regular Go programs. If the pipeline is empty, the default value of if is false. The following example shows how to use if-else in templates:

package main

import (
	"os"
	"text/template"
)

func main() {
	tEmpty := template.New("template test")
	tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} will not be outputted. {{end}}\n"))
	tEmpty.Execute(os.Stdout, nil)

	tWithValue := template.New("template test")
	tWithValue = template.Must(tWithValue.Parse("Not empty pipeline if demo: {{if `anything`}} will be outputted. {{end}}\n"))
	tWithValue.Execute(os.Stdout, nil)

	tIfElse := template.New("template test")
	tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if part {{else}} else part.{{end}}\n"))
	tIfElse.Execute(os.Stdout, nil)
}

As you can see, it's easy to use if-else in templates.

Attention You CANNOT use conditional expressions in if, for instance .Mail=="[email protected]". Only boolean values are acceptable.

pipelines

Unix users should be familiar with the pipe operator, like ls | grep "beego". This command filters files and only shows those that contain the word beego. One thing that I like about Go templates is that they support pipes. Anything in {{}} can be the data of pipelines. The e-mail we used above can render our application vulnerable to XSS attacks. How can we address this issue using pipes?

{{. | html}}

We can use this method to escape the e-mail body to HTML. It's quite similar to writing a Unix command, and it is convenient for use in template functions.

Template variables

Sometimes we need to use local variables in templates. We can use them with the with, range and if keywords, and their scope is between these keywords and {{end}}. Here's an example of declaring a global variable:

$variable := pipeline

More examples:

{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

Template functions

Go uses the fmt package to format output in templates, but sometimes we need to do something else. For example consider the following scenario: let's say we want to replace @ with at in our e-mail address, like astaxie at beego.me. At this point, we have to write a customized function.

Every template function has a unique name and is associated with one function in your Go program as follows:

type FuncMap map[string]interface{}

Suppose we have an emailDeal template function associated with its EmailDealWith counterpart function in our Go program. We can use the following code to register this function:

t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})

EmailDealWith definition:

func EmailDealWith(args …interface{}) string

Example:

package main

import (
	"fmt"
	"html/template"
	"os"
	"strings"
)

type Friend struct {
	Fname string
}

type Person struct {
	UserName string
	Emails   []string
	Friends  []*Friend
}

func EmailDealWith(args ...interface{}) string {
	ok := false
	var s string
	if len(args) == 1 {
		s, ok = args[0].(string)
	}
	if !ok {
		s = fmt.Sprint(args...)
	}
	// find the @ symbol
	substrs := strings.Split(s, "@")
	if len(substrs) != 2 {
		return s
	}
	// replace the @ by " at "
	return (substrs[0] + " at " + substrs[1])
}

func main() {
	f1 := Friend{Fname: "minux.ma"}
	f2 := Friend{Fname: "xushiwei"}
	t := template.New("fieldname example")
	t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
	t, _ = t.Parse(`hello {{.UserName}}!
				{{range .Emails}}
					an emails {{.|emailDeal}}
				{{end}}
				{{with .Friends}}
				{{range .}}
					my friend name is {{.Fname}}
				{{end}}
				{{end}}
				`)
	p := Person{UserName: "Astaxie",
		Emails:  []string{"[email protected]", "[email protected]"},
		Friends: []*Friend{&f1, &f2}}
	t.Execute(os.Stdout, p)
}

Here is a list of built-in template functions:

var builtins = FuncMap{
	"and":      and,
	"call":     call,
	"html":     HTMLEscaper,
	"index":    index,
	"js":       JSEscaper,
	"len":      length,
	"not":      not,
	"or":       or,
	"print":    fmt.Sprint,
	"printf":   fmt.Sprintf,
	"println":  fmt.Sprintln,
	"urlquery": URLQueryEscaper,
}

Must

The template package has a function called Must which is for validating templates, like the matching of braces, comments, and variables. Let's take a look at an example of Must:

package main

import (
	"fmt"
	"text/template"
)

func main() {
	tOk := template.New("first")
	template.Must(tOk.Parse(" some static text /* and a comment */"))
	fmt.Println("The first one parsed OK.")

	template.Must(template.New("second").Parse("some static text {{ .Name }}"))
	fmt.Println("The second one parsed OK.")

	fmt.Println("The next one ought to fail.")
	tErr := template.New("check parse error with Must")
	template.Must(tErr.Parse(" some static text {{ .Name }"))
}

Output:

The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command

Nested templates

Just like in most web applications, certain parts of templates can be reused across other templates, like the headers and footers of a blog. We can declare header, content and footer as sub-templates, and declare them in Go using the following syntax:

{{define "sub-template"}}content{{end}}

The sub-template is called using the following syntax:

{{template "sub-template"}}

Here's a complete example, supposing that we have the following three files: header.tmpl, content.tmpl and footer.tmpl in the folder templates, we will read the folder and store the file names in a string array, which we will then use to parse files.

Main template:

{% raw %}
//header.tmpl
{{define "header"}}
<html>
<head>
	<title>Something here</title>
</head>
<body>
{{end}}

//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>Nested here</h1>
<ul>
	<li>Nested usag</li>
	<li>Call template</li>
</ul>
{{template "footer"}}
{{end}}

//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}

//When using subtemplating make sure that you have parsed each sub template file,
//otherwise the compiler wouldn't understand what to substitute when it reads the {{template "header"}}

{% endraw %}

Code:

package main

import (
	"fmt"
	"os"
	"io/ioutil"
	"text/template"
)

var templates *template.Template

func main() {
	var allFiles []string
	files, err := ioutil.ReadDir("./templates")
	if err != nil {
		fmt.Println(err)
	}
	for _, file := range files {
		filename := file.Name()
		if strings.HasSuffix(filename, ".tmpl") {
			allFiles = append(allFiles, "./templates/"+filename)
		}
	}

	templates, err = template.ParseFiles(allFiles...) #parses all .tmpl files in the 'templates' folder

	s1 := templates.Lookup("header.tmpl")
	s1.ExecuteTemplate(os.Stdout, "header", nil)
	fmt.Println()
	s2 := templates.Lookup("content.tmpl")
	s2.ExecuteTemplate(os.Stdout, "content", nil)
	fmt.Println()
	s3 := templates.Lookup("footer.tmpl")
	s3.ExecuteTemplate(os.Stdout, "footer", nil)
	fmt.Println()
	s3.Execute(os.Stdout, nil)
}

Here we can see that template.ParseFiles parses all nested templates into cache, and that every template defined by {{define}} are independent of each other. They are persisted in something like a map, where the template names are keys and the values are the template bodies. We can then use ExecuteTemplate to execute the corresponding sub-templates, so that the header and footer are independent and content contains them both. Note that if we try to execute s1.Execute, nothing will be outputted because there is no default sub-template available.

When you don't want to use {{define}}, then you can just create a text file with the name of the sub template, for instance _head.tmpl is a sub template which you'll use across your project then create this file in the templates folder, and use the normal syntax. Lookup cache is basically created so that you don't read the file every time you serve a request, because if you do, then you are wasting a lot of resources for reading a file which won't change unless the codebase is being rewritten, it doesn't make sense to parse the template files during each HTTP GET request, so the technique is used where we parse the files once and then do a Lookup() on the cache to execute the template when we need it to display data.

Templates in one set know each other, but you must parse them for every single set.

Some times you want to contextualize templates, for instance you have a _head.html, you might have a header who's value you have to populate based on which data you are loading for instance for a todo list manager you can have three categories pending, completed, deleted. for this suppose you have an if statement like this

<title>{{if eq .Navigation "pending"}} Tasks
    {{ else if eq .Navigation "completed"}}Completed
    {{ else if eq .Navigation "deleted"}}Deleted
    {{ else if eq .Navigation "edit"}} Edit
    {{end}}
</title>

Note: Go templates follow the Polish notation while performing the comparison where you give the operator first and the comparison value and the value to be compared with. The else if part is pretty straight forward

Typically we use a {{ range }} operator to loop through the context variable which we pass to the template while execution like this:

    //present in views package
    context := db.GetTasks("pending") //true when you want non deleted notes
    homeTemplate.Execute(w, context)

We get the context object from the database as a struct object, the definition is as below

//Task is the struct used to identify tasks
type Task struct {
   	Id      int
    Title   string
    Content string
    Created string
}
//Context is the struct passed to templates
type Context struct {
    Tasks      []Task
    Navigation string
    Search     string
    Message    string
}

//present in database package
var task []types.Task
var context types.Context
context = types.Context{Tasks: task, Navigation: status}

//This line is in the database package where the context is returned back to the view.

We use the task array and the Navigation in our templates, we saw how we use the Navigation in the template, we'll see how we'll use the actual task array in our template.

Here in the {{ if .Tasks }} we first check if the Tasks field of our context object which we passed to the template while executing is empty or not. If it is not empty then we will range through that array to populate the title and content of Task. The below example is very important when it comes to looping through an array in a template, we start with the Range operator, then we can give any member of that struct as {{.Name}}, my Task structure has a Title and a Content, (please note the capital T and C, they are exported names and they need to be capitalised unless you want to make them private).

{{ range .Tasks }}
    {{ .Title }}
    {{ .Content }}
{{ end }}

This block of code will print each title and content of the Task array. Below is a full example from github.com/thewhitetulip/Tasks home.html template.

<div class="timeline">
{{ if .Tasks}} {{range .Tasks}}
<div class="note">
    <p class="noteHeading">{{.Title}}</p>
    <hr>
    <p class="noteContent">{{.Content}}</p>
    </ul>
    </span>
</div>
{{end}} {{else}}
<div class="note">
    <p class="noteHeading">No Tasks here</p>
    <p class="notefooter">
    Create new task<button class="floating-action-icon-add" > here </button> </p>
</div>
{{end}}

Summary

In this section, you learned how to combine dynamic data with templates using techniques including printing data in loops, template functions and nested templates. By learning about templates, we can conclude discussing the V (View) part of the MVC architecture. In the following chapters, we will cover the M (Model) and C (Controller) aspects of MVC.

Links