-
Notifications
You must be signed in to change notification settings - Fork 110
3. Jet template syntax
- Execution Basics
- Delimiters
- Accessing Input
- Traversing Data
- Expressions
- Control Structures
- Blocks
- "Include" Statement
- "Import" Statement
- "Extend" Statement
- Relative Template Lookup
Executing a template requires you to pass a variable map and a context:
func (t *Template) Execute(w io.Writer, variables VarMap, data interface{}) error
Calling it could look like this:
var viewSet = jet.NewHTMLSet("./views"))
t, err := viewSet.GetTemplate(templateName)
if err != nil {
// handle error
}
context := struct{
Data string
}{
Data: "apples",
}
var w bytes.Buffer // (or gin's context.Writer, for example)
vars := make(jet.VarMap)
if err = t.Execute(&w, vars, context); err != nil {
// error when executing template
}
(This is covered further in Rendering Templates.)
When a template renders, you have access to three types of dynamic inputs:
- Global variables and functions which are defined on your
Set
of templates. - The
jet.VarMap
which can contain variables unique to the rendering pass. - The context, an
interface{}
which can be whatever you like. The context is useful because you can change its meaning during an execution pass, such as when rendering a sub-template (called a{{block}}
).
By default, the template delimiters are {{
and }}
. You can change them using the Delims("[[", "]]")
method on the Set
:
var viewSet = jet.NewHTMLSet("./views"))
viewSet.Delims("[[", "]]")
Then your templates look like this:
<!-- file: "views/home.jet" -->
[[extends "layouts/application.jet"]]
[[block body()]]
<main>
This content will be yielded in the layout above.
</main>
[[end]]
After that, you can also then use {{
and }}
for templates that are used on the client side via VueJS or the like. Make sure to test your templates after changing the delimiters.
Delimiters for a Set are set for all included/yielded/imported templates and partials so mixing is not possible.
Comments begin with {*
and end with *}
.
{* this is a comment *}
Add global variables to your jet.Set
and access them like this:
viewSet.AddGlobal("version", "v1.1.145")
<footer>Version: {{ version }}</footer>
Similar to global variables:
viewSet.AddGlobal("noblue", func(s string) string {
return strings.Replace(s, "blue", "", -1)
})
// If speed is a consideration, use GlobalFunc instead
viewSet.AddGlobalFunc("noblue", func(args jet.Arguments) reflect.Value {
a.RequireNumOfArguments("noblue", 1, 1)
return reflect.ValueOf(strings.Replace(s, "blue", "", -1))
})
<div>{{ noblue("red blue green") }}</div>
<!-- Alternate call syntax, using pipelining (see below) -->
<div>{{ "red blue green"|noblue }}</div>
For a list of functions that are already available, see Built-In Functions.
For more information on GlobalFunc
, also called "fast functions", see Advanced Customization.
Use the convenience method Set(key, value)
to add variables to your varmap, and access them by name:
vars := make(jet.VarMap)
vars.Set("user", &User{})
<div>{{ user }}</div>
The context is represented as a .
and its value is relative to the template or block being executed. At the top-level, it would be whatever is passed into Execute
.
data := &http.Request{Method: http.MethodPost}
err := t.Execute(&w, vars, data)
// ...
<div>Request method is: {{ .Method }}</div> <!-- Would be "POST" -->
Access members and call methods:
The user's firstname is {{ user.Firstname }}.
The user's name is {{ user.Fullname() }}.
Now his name is {{ user.Rename("Frederick", "Johnson") }}.
Access maps with the square-brace syntax:
The test map's value is {{ testMap["testKey"] }}.
Use the two-value return syntax to check if a key is set:
{{if v, ok := testMap["testKey"]; ok}}
{{ v }} <!-- will print "test-value" -->
{{end}}
Accessing by index is the same for both types:
The test slice's value is {{ testSlice[0] }}.
{{range .}}
{{end}}
{* you can do an assign in a range statement;
this value will be assigned in every iteration *}
{{range value := .}}
{{end}}
{* the syntax is the same for maps as well as slices *}
{{range keyOrIndex, value := .}}
{{end}}
{* Ranging over a collection changes the context to each item in turn. *}
{{range favoriteColors}}
<p>I like {{.}}</p>
{{end}}
Finally, ranges can have an else
block which are executed when the range has no iterations (empty map or slice):
{{range index, value := .}}
{{else}}
{{end}}
If your custom type satisfies the jet.Ranger
interface, Jet will use your custom method to range over your type.
The method you need to implement has this signature:
Range() (reflect.Value, reflect.Value, bool)
Basic arithmetic operators are supported: +
, -
, *
, /
, %
{{ 1 + 2 * 3 - 4 }} <!-- will print 3 -->
{{ (1 + 2) * 3 - 4.1 }} <!-- will print 4.9 -->
{{ "HELLO"+" WORLD!" }} <!-- will print "HELLO WORLD!" -->
{{ if item == true || !item2 && item3 != "test" }}
{{ if item >= 12.5 || item < 6 }}
{{ item := items[1] }}
{{ item2 := .["test"] }} <!-- also works on the context -->
{{ .HasTitle ? .Title : "Title not set" }}
You may range over parts of a slice using the Go-like [start:end] syntax. The end is non-inclusive.
{{range v := .[1:3]}}{{ v }}{{end}}
<!-- context is []string{"0", "1", "2", "3"}, will print "12" -->
Simple strings can be printed or transformed via pipelines.
{{ "HELLO WORLD"|lower }} <!-- will print "hello world" -->
Pipelines can be expressed in a few different ways:
chaining: {{ "HELLO"|lower|repeat:2 }} (will print hellohello)
prefix: {{ lower:"HELLO"|upper|repeat:2 }} (will print HELLOHELLO)
simple function call: {{ lower("HELLO") }}
As you can see, chaining is easier when using the pipe, pipes also accept parameters, and the prefix call has a lower precedence than the pipe.
Please note that when you're using a custom escape handler or the raw/unsafe filters these need to be passed last. In the examples above they can only be used as the last pipeline. If you're using the prefix version then there cannot be any pipelines in the statement:
{{ "hello"|upper|raw }} <!-- valid -->
{{ raw:"hello" }} <!-- valid -->
{{ raw:"hello"|upper }} <!-- invalid -->
{{if expression}}
{{end}}
{* assignment is possible as well *}
{{if ok := expression; ok }}
{{else if expression}}
{{else}}
{{end}}
These work exactly the same as in Go.
You can think of blocks as partials or pieces of a template that you can invoke by name.
<!-- Define a block -->
{{block copyright()}}
<div>© ACME, Inc. 2018</div>
{{end}}
<!-- Invoke with "yield" -->
<footer>
{{yield copyright()}}
</footer>
<!-- Output -->
<footer>
<div>© ACME, Inc. 2018</div>
</footer>
Keep in mind that the place where you define a block is also the place where it's invoked (see Define & Invoke Together). If that's not what you want then you have to put it in another template and import it, see Import Statement.
{{block inputField(type="text", label, id, value="", required=false)}}
<div class="form-field">
<label for="{{ id }}">{{ label }}</label>
<input type="{{ type }}" value="{{ value }}" id="{{ id }}" {{if required}}required{{end}} />
</div>
{{end}}
A block definition accepts a comma-separated list of argument names, with optional defaults.
Default values can be almost any expression, including:
- Local or global variables (i.e. for form errors)
- An
isset
call - Function expressions
Blocks can't be named content
, yield
, or other Jet keywords.
{{yield inputField(id="firstname", label="First name", required=true)}}
Use yield
to invoke a block by name. The sequence of parameters is irrelevant, and parameters without a default value must be passed when yielding a block.
You can pass an expression to be used as the context, or the current context will be passed.
<!-- Define block -->
{{block buff()}}
<strong>{{.}}</strong>
{{end}}
<!-- Invoke & provide a context -->
{{yield buff() "Man"}}
<!-- Output -->
<strong>Man</strong>
<!-- Define a block which calls itself -->
{{block menu()}}
<ul>
{{range .}}
<li>{{ .Text }}{{if len(.Children)}}{{yield menu() .Children}}{{end}}</li>
{{end}}
</ul>
{{end}}
<!-- Invoke -->
<nav>
{{yield menu() navItems}}
</nav>
When defining a block, use the special {{yield content}}
statement to designate where any inner content should be rendered. Then, when you invoke the block with yield
, pass the keyword content
as the context. Here is an example:
<!-- Define block -->
{{block link(target)}}
<a href="{{target}}">{{yield content}}</a>
{{end}}
<!-- Invoke -->
{{yield link(target="https://www.example.com") content}}
Example.com
{{end}}
<!-- Output -->
<a href="https://www.example.com">Example.com</a>
Here is a more complex example:
<!-- Define block -->
{{block cols(class="col-md-12", wrapInContainer=false, wrapInRow=true)}}
{{if wrapInContainer}}<div class="container">{{end}}
{{if wrapInRow}}<div class="row">{{end}}
<div class="{{ class }}">{{yield content}}</div>
{{if wrapInRow}}</div>{{end}}
{{if wrapInContainer}}</div>{{end}}
{{end}}
<!-- Invoke -->
{{yield cols(class="col-xs-12") content}}
<p>This content will be wrapped.</p>
{{end}}
<!-- Output -->
<div class="row">
<div class="col-xs-12">
<p>This content will be wrapped.</p>
</div>
</div>
Performance when using this extensively is good because the blocks are parsed and cached upon the first import.
{{block pageHeader() .Header}}
{{ .Title }}
{{end}}
When you define a block in a template that's not imported and pass an additional expression like ".Header" this is equivalent to: define the block and invoke the block with expression.
Including a template is the same as using partials in other template languages. All local and global variables are available to you in the included template. You can optionally pass a context by specifying it as the last argument in the {{include}}
statement:
<!-- file: "views/users/_user.jet" -->
<div class="user">
{{ .Firstname }} {{ .Lastname }}: {{ .Email }}
</div>
<!-- file: "views/users/index.jet" -->
{{range user := users}}
{{include "users/_user.jet" user}}
{{end}}
If you're not sure whether the template you want to include does actually exist, you can use the {{includeIfExists}}
statement. It does not panic or throw an error when the template does not exist. It also comes in a second form that returns a boolean so you can react to the template not existing and fall back to default content.
{{includeIfExists "sidebars/"+user.ID+".jet" user}} <!-- no content if the template does not exist -->
{{if ok := includeIfExists("sidebars/"+user.ID, user); !ok}}
<p>The template does not exist.</p>
{{end}}
{{end}}
You may also use this in a {{block}}
definition.
Importing a template makes the defined blocks available for you to yield and use in your template:
<!-- file: "views/common/_menu.jet" -->
{{block menu()}}
<ul>
{{range .}}
<li>{{ .Text }}{{if len(.Children)}}{{yield menu() .Children}}{{end}}</li>
{{end}}
</ul>
{{end}}
<!-- file: "views/home.jet" -->
{{import "common/_menu.jet"}}
{{yield menu() navItems}}
<main>
Content.
</main>
Extending a template essentially means wrapping it with a "layout". Here is an example of a layout:
<!-- file: "views/layouts/application.jet" -->
<!DOCTYPE html>
<html>
<head>
<title>{{yield title()}}</title>
</head>
<body>
{{yield body()}}
</body>
</html>
You can then extend the layout in a primary template, overriding block definitions that will be yielded in the layout:
<!-- file: "views/home.jet" -->
{{extends "layouts/application.jet"}}
{{block title()}}My title{{end}}
{{block body()}}
<main>
This content will be yielded in the layout above.
</main>
{{end}}
The result would be:
<!DOCTYPE html>
<html>
<head>
<title>My title</title>
</head>
<body>
<main>
This content will be yielded in the layout above.
</main>
</body>
</html>
When extending a template it's required that the {{extends}}
statement is the first statement in the template. You may only extend one template.
Given the directory tree described in the Rendering templates section it's possible to import and include templates relative to the template that's importing/including them:
<!-- file: "views/auth/_logo.jet" -->
<img src="{{ .LogoURL }}" />
<!-- file: "views/auth/login.jet" -->
{{extends "layouts/application.jet"}}
{{block body()}}
{{include "_logo" company}}
{{end}}
You may leave out the template filename's extension if it ends with .jet
, .html.jet
or .jet.html
.