Skip to content

3. Jet template syntax

Matt Mc edited this page Jul 13, 2018 · 31 revisions

Table of Contents

  1. Execution Basics
  2. Delimiters
  3. Accessing Input
  4. Traversing Data
  5. Expressions
  6. Control Structures
  7. Blocks
  8. "Include" Statement
  9. "Import" Statement
  10. "Extend" Statement
  11. Relative Template Lookup

Execution Basics

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 set = 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}}).

Delimiters

By default, the templating delimiters are {{ and }}. Soon you can change this using the Delims method.

Comments begin with {* and end with *}.

{* this is a comment *}

Accessing Input

Global Variables

Add global variables to your jet.Set and access them like this:

viewSet.AddGlobal("version", "v1.1.145")
<footer>Version: {{ version }}</footer>

Global Functions

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.

Varmap

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>

Context

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

Traversing Data

Structs

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") }}.

Maps

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

Slices and Arrays

Accessing by index is the same for both types:

The test slice's value is {{ testSlice[0] }}.

Range over maps and slices

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

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

Custom Ranging

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)

Expressions

Arithmetic

Basic arithmetic operators are supported: +, -, *, /, %

{{ 1 + 2 * 3 - 4 }} <!-- will print 3 -->
{{ (1 + 2) * 3 - 4.1 }} <!-- will print 4.9 -->

String Concatenation

{{ "HELLO"+" WORLD!" }} <!-- will print "HELLO WORLD!" -->

And, or, not, equals, comparison operators

{{ if item == true || !item2 && item3 != "test" }}

{{ if item >= 12.5 || item < 6 }}

Variable Declaration

{{ item := items[1] }}
{{ item2 := .["test"] }} <!-- also works on the context -->

Tenary Expression

{{ .HasTitle ? .Title : "Title not set" }}

Slice Expression

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

Pipelining

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

Control Structures

if, else, and else if

{{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.

Blocks

You can think of blocks as partials or pieces of a template that you can invoke by name.

Simple Example

<!-- 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.

Defining a Block

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

Invoking a Block

{{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 strong()}}
  <strong>{{.}}</strong>
{{end}}

<!-- Invoke -->
{{yield strong() "Man"}}

<!-- Output -->
<strong>Man</strong>

Wrapping While Invoking

When defining a block, use yield content to designate where any inner content should be rendered, and invoke the block with yield block() content, as follows:

<!-- 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.

Define & Invoke Together

{{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.

Include Statement

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 and passing a context can be done by specifying it as the last argument in the `{{include}}`` statement (as always, the context is optional):

<!-- file: "views/users/_user.jet" -->
<div class="user">
  {{ .Firstname }} {{ .Lastname }}: {{ .Email }}
</div>

<!-- file: "views/users/index.jet" -->
{{extends "layouts/application.jet"}}
{{block body()}}
  {{range user := users}}
    {{include "users/_user.jet" user}}
  {{end}}
{{end}}

IncludeIfExists

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.

{{extends "layouts/application.jet"}}
{{block body()}}
  {{includeIfExists "sidebars/"+user.ID+".jet" user}} <!-- no content if the template does not exist -->

  {{if ok := includeIfExists("sidebars/"+user.ID, user); !ok}} <!-- the second argument is the context (optional) -->
    <p>The template does not exist.</p>
  {{end}}
{{end}}

You may also use this in a {{block}} definition.

Import Statement

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 .}} {* set the context appropriately *}
      <li>{{ .Text }}{{if len(.Children)}}{{yield menu() .Children}}{{end}}</li>
    {{end}}
  </ul>
{{end}}

<!-- file: "views/home.jet" -->
{{extends "layouts/application.jet"}}
{{import "common/_menu.jet"}}
{{block body()}}
  {* yield invokes the block by name; you can pass an expression to be used as the context or the current context is passed *}
  {{yield menu() navItems}}
  <main>
    Content.
  </main>
{{end}}

Extend Statement

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.

Relative Template Lookup

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.

Clone this wiki locally