Skip to content

V1 Documentation

Daniel Lohse edited this page Aug 12, 2016 · 4 revisions

Documentation for Jet v1

This is the documentation for Jet v1. You can find the documentation for v2 here.

Jet is a template engine that was designed to be easy to use and fast.

  • simple and familiar syntax
  • easy to use
  • dynamic
  • fast and light
  • useful error messages
  • template inheritance
  • powerful

Simple and familiar syntax

The syntax has many similarities with Go's text/template templating language. The main differences are:

  • support for template inheritance
  • simple C-like expressions
  • pipelines can only be used in actions (i.e. {{ expression|pipe|pipe }})
  • all templates are file based – there are no "define", "template" or "with" actions

Dynamic

Templates can extend a template (known as a layout) and import blocks from another and everything just works, that way you can create a template library, layout templates and the application templates.

Fast and light

The engine was designed to be fast and light by avoiding unnecessary allocations and a fast runtime.

Useful error messages

All error messages are tied to the file and line of the node executing the action or expression, and all message are descriptive so you know exactly what's wrong.

Template inheritance

In Jet you can extend, import and include templates:

  • when extending, all blocks from the template that you extend will be available for your template even the ones that it imports or extends, also the root of the extended template is used as the entry point when executing
  • when importing a template, all blocks of the imported template will be available in your template
  • When including a template, the template will be invoked and all blocks available in your template will be available in the included template

Getting started

Getting started is easy and consists of getting the package, initializing a Set with a path to the templates and then rendering your first template.

  1. Get the package
  $ go get -u github.com/CloudyKit/jet

You may also use your favorite tool to vendor the library (git-freeze, git submodule).

  1. Create a Set and specify the lookup directories
  import (
      "os"
      "path/filepath"

      "github.com/CloudyKit/jet"
  )

  var View = jet.NewHTMLSet("./views")) // relative path to the Go file where this code is located

  // may also use an absolute path:
  var root, _ = os.Getwd()
  var View = jet.NewHTMLSet(filepath.Join(root, "views"))
  1. Create a layout and your first template
  <!-- file: "views/layouts/application.jet" -->
  <!DOCTYPE html>
  <html>
    <head></head>
    <body>
      {{yield body}}
    </body>
  </html>

  <!-- file: "views/home.jet" -->
  {{extends "layouts/application.jet"}}
  {{block body}}
    <main>
      This content will be yielded in the layout above.
    </main>
  {{end}}
  1. Execute the template. You'll be providing the template with variables (map[string]interface{}), data (interface{}) and most importantly, an io.Writer for the template to be rendered into. Anything that conforms to that interface can be passed; examples include a simple bytes.Buffer, Gin's context.Writer, or http.ResponseWriter in the case of Goji or if you're using the standard library's http package.
  templateName := "home.jet"
  t, err := View.GetTemplate(templateName)
  if err != nil {
      // template could not be loaded
  }
  var w bytes.Buffer // needs to conform to io.Writer interface (like gin's context.Writer for example)
  vars := map[string]interface{}
  if err = t.Execute(&w, vars, nil); err != nil {
      // error when executing template
  }

Template execution is synchronous and w will contain the rendered template's content. We didn't have any variables or data in this simple example so the vars map was empty (can also just pass nil here), as was the data (last parameter). Learn more about loading and executing templates in the section Rendering templates below.

Now that you know the basics, read on for a closer look at the syntax, learn how to add global variables and functions and see all the built-in functions available to you in every template.

Jet template syntax

The following examples use this struct definition when referring to the user variable:

// User model
type User struct {
    Email     string
    Firstname string
    Lastname  string
}

func (u *User) Fullname() string {
    return fmt.Sprintf("%s %s", u.Firstname, u.Lastname)
}

It was passed to the template when executing it like this:

vars := map[string]interface{
  "user":      &User{Firstname: "Michael", Lastname: "Jones"},
  "testMap":   map[string]string{"testKey": "value"},
  "testSlice": []string{"value1", "value2"},
}
t.Execute(w, vars, nil)

Learn more about the variable map and the context in the section Rendering templates below.

Comments

Comments begin with {* and end with *}.

  {* this is a comment *}

Function calls

When passing data via the variable map or the context, functions can be called like in Go.

  The user's name is {{ user.Fullname() }}.

You may also pass parameters to these functions just like in Go.

Accessing struct members, maps, slices, and arrays

Accessing struct members:

  The user's firstname is {{ user.Firstname }}.

Accessing maps, slices, and arrays is the same as in Go:

The test map's value is {{ testMap["testKey"] }}.
The test slice's value is {{ testSlice[0] }}.
// accessing arrays is the same is accessing a slice but should rarely be needed

Iterating over maps and slices with range

range is like a for range in Go:

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

if, else, and else if

Finally, you have the if control structures at your disposal which work exactly the same way as in Go:

{{if expression}}
{{end}}

{* assignment is possible as well *}
{{if ok := expression; ok }}

{{else if expression}}

{{else}}

{{end}}

isset / len

See the section Built-in functions below.

Defining blocks

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

{{block menu}}
  <ul>
    {{range items}} {* `items` must be defined before this block is executed via `yield` *}
      <li>{{ .Text }}{{if len(.Children)}}{{yield menu .Children}}{{end}}</li>
    {{end}}
  </ul>
{{end}}

Keep in mind that the place where you define a block is also the place where it's invoked; if that's not what you want then you have to put it in another template and import it. Read on in the next section to better understand what this means.

Extending, importing, and yielding

Finally, let's cover extending, importing, and including a template as well as yielding blocks.

Extending a template essentially means wrapping it with a layout. When extending a template it's required that the {{extends "layouts/application.jet"}} statement is the first statement in the template. You may also only extend one template.

When you yield blocks in a layout you may override those blocks in your template. This can be used, for example, to define the HTML title as well as the main body:

<!-- file: "views/layouts/application.jet" -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{yield title}}</title>
  </head>
  <body>
    {{yield body}}
  </body>
</html>

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

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

One example to define a block and immediately invoke it:

{* 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 *}
{{block pageHeader .Header}}
{{end}}

Global variables and functions

For a list of all built-in functions see the section Built-in functions below.

Global variables and functions are added directly to the Set you created and initialized with the template lookup directories. The Set should only be created once and be available for the duration of your program's runtime. Anything you add must not depend on individual requests and also be safe to be called from multiple goroutines simultaneously.

Adding a global function is easy. Consider this function to uppercase the first character in a string:

View.AddGlobal("ucfirst", func(str string) string {
    if firstChar, _ := utf8.DecodeRuneInString(str); firstChar != utf8.RuneError {
      strRunes := []rune(str)
      strRunes[0] = unicode.ToUpper(firstChar)
      return string(strRunes)
    }
    return str
})

This may be called in a template in a couple of ways:

<h1>{{ "test"|ucfirst }}</h1>  <!-- "Test" -->
<h1>{{ ucfirst("test") }}</h1> <!-- "Test" -->

You may also add more parameters which need to be passed from the template when invoking the function:

View.AddGlobal("default", func(str, defaultStr string) string {
    if len(str) == 0 {
       return defaultStr
    }
    return str
})
<h1>{{ ""|default:"default" }}</h1>          <!-- "default" -->
<h1>{{ default("", "default string") }}</h1> <!-- "default string" -->

Global variables (for example a version or build number you want to show in a footer) are pretty much the same:

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

Built-in functions

Some functions are available to you in every template. They may be in invoked as regular functions (lower("TEST") // "test") or, which is preferred, as pipelines which also allows chaining: {{ "test "|upper|trimSpace }} // "TEST".

For documentation on how to add your own (global) functions see the section Global variables and functions above.

isset / len

These are frequently used. isset can be used to check against truthy or falsy expressions, whereas len can count elements in arrays, channels, slices, maps, and strings. When used on a struct argument it returns the number of fields.

from go's strings package

  • lower (strings.ToLower)
  • upper (strings.ToUpper)
  • hasPrefix (strings.HasPrefix)
  • hasSuffix (strings.HasSuffix)
  • repeat (strings.Repeat)
  • replace (strings.Replace)
  • split (strings.Split)
  • trimSpace (strings.TrimSpace)

escape helper

  • html (html.EscapeString)
  • url (url.QueryEscape)
  • safeHtml (escape HTML)
  • safeJs (escape JavaScript)
  • raw, unsafe (no escaping)
  • writeJson, json to dump variables as JSON strings

on-the-fly map creation

  • map: map(key1, value1, key2, value2) – use with caution because accessing these is slow when used with lots of elements and checked/read in loops.

Rendering templates

In the Getting started section at the beginning we had this piece of code as the last step to execute a template:

  templateName := "home.jet"
  t, err := View.GetTemplate(templateName)
  if err != nil {
      // template could not be loaded
  }
  var w bytes.Buffer // needs to conform to io.Writer interface (like gin's context.Writer for example)
  vars := map[string]interface{}
  if err = t.Execute(&w, vars, nil); err != nil {
      // error when executing template
  }

What's the vars map there as the second parameter? And why did we pass nil as the third parameter? How are templates located and loaded? Let's start there.

Loading a template

When you instantiate a Set and give it the directories for template lookup it will not search them right away. Templates are located and loaded on-demand.

Imagine this tree of templates in your project folder:

├── main.go
├── README.md
└── views
    ├── common
    │   ├── _footer.jet
    │   └── _menu.jet
    ├── auth
    │   └── login.jet
    ├── home.jet
    └── layouts
        └── application.jet

The Set might have been initialized in the main.go like this:

  var View = jet.NewHTMLSet("./views"))

Jet loads templates relative to the lookup directory; to load the login.jet template you'd do:

  t, err := View.GetTemplate("auth/login.jet")

Loading a template parses it and all included, imported or extended templates – and caches the result so parsing only happens once.

Reloading a template in development

While developing a website or web app in Go it'd be nice to not cache the result after loading a template so you can leave your Go app running and still make incremental changes to the template(s). For this Jet includes a development mode which disables caching the templates:

  View.SetDevelopmentMode(true)

Be advised to disable the development mode on staging and in production to achieve maximum performance.

Passing variables when executing a template

When executing a template you are passing the io.Writer object as well as the variable map and a context. Both of these will be explained next.

The variable map is a map[string]interface{} for variables you want to access by name in your templates. You may also use the provided jet.VarMap which has a convenience method Set(key, value):

  vars := make(jet.VarMap)
  vars.Set("user", &User{})

You usually build up the variable map in one of your controller functions in response to an HTTP request by the user.

Lastly, the context: the context is passed as the third parameter to the t.Execute template execution function and is accessed in the template using the dot. Anything can be used as a context but if you are rendering a user edit form it'd be best to pass the user as the context.

<form action="/user" method="post">
  <input name="firstname" value="{{ .Firstname }}" />
</form>

Using a context can also be helpful when making blocks more reusable because the context can change while the template stays the same: {{ .Text }}.

Expressions

Jet understands a lot of expressions. Some you've already seen throughout the documentation:

  • executing functions via {{ object.Function() }}
  • accessing struct members via {{ struct.Property }}
  • accessing maps, arrays, slices via ["key"] / [0]
  • accessing the context with the dot operator and executing functions as well as access properties, members and keys shown above

Arithmetic

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

Additionally, you may use / and %.

And, or, not, equals, comparison operators

Boolean operations for use in control structures.

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

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

Variable declaration

Go-like variable declaration is also supported.

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

Strings

Simple strings can be printed or transformed via pipelines.

{{ "HELLO WORLD"|lower }} <!-- will print "hello world" -->

Pipelines

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.

Ternary expression

{{ isset(.Title) ? .Title : "Title not set" }}

Slice expressions

You may range over parts of a slice using the Go-like [start:end] syntax. The end is

{{range v := .[1:3]}}{{ v }}{{end}} <!-- context is []string{"0", "1", "2", "3"}, will print "12" -->