Programming in Twig

http://marion.newlevant.com/programming-in-twig/

twig code

Twig syntax

{# comments #}

{{ output }}

{% tags %}

Twig Values

  • strings: 'single quotes' "double quotes"
    ~ is the string concatination operator: 'foo'~'bar' => 'foobar'
  • numbers: 1, 2.5, -7
  • booleans: true and false
  • arrays: ['foo', 23, []]
  • associative arrays: {key: 'value', anotherKey: [1, 2, 'frogs']}
  • and really, any php value

Operators

  • The usual (+, -, and, or, not, ==, <, >, etc.) plus filters and functions.
  • filters example:
    • ['a', 'b']|join(', ')
      => 'a, b'
    • ['a', 'b']|join(', ')|upper
      => 'A, B'
  • functions example: max(foo, bar, 0)

Who defines these things?

  • twig core
  • craft
  • plugins

Twig documentation

Craft Filters, functions and tags

Filters:  currency, datetime, filesize, filter, group, indexOf, intersect, lcfirst, markdown (md), number, parseRefs, percentage, replace, translate (t), ucfirst, ucwords, without

Functions:  ceil, floor, getCsrfInput, getFootHtml, getHeadHtml, shuffle, url

Tags:  cache, exit, header, includeCss, includeCssFile, includeHiResCss, includeJs, includeJsFile, nav, paginate, redirect, requireLogin, requirePermission, switch

Control Structures

  • output
  • assignment
  • conditionals
  • looping/iterating
  • functions (macros)
  • includes
  • extends and embeds

Output


{{ value }}
    

Convert value to a string

Print that string out inline


{{ 'hello, world'|upper }}
    

HELLO, WORLD
    

Output: dump


{{ dump(foo) }}
    

All the details (var_dump) about foo, printed out. Need to have devMode enabled for this to work.


{{dump([1, 2, 'fish'])}}
    

array(3) {
  [0]=> int(1)
  [1]=> int(2)
  [2]=> string(4) "fish"
}    

Assignment


{% set foo = bar %}
  

foo will have whatever type bar has


{% set foo %}
  ...bunch of template code...
{% endset %}
    

foo will be a string

Conditionals - If


{% if someValue %}
  ...
{% elseif someOtherValue %}
  ...
{% else %}
  ...
{% endif %}
    

Things that are false: false, 0, '', []

Things that are true: everything else

Conditionals - Switch


{% switch matrixBlock.type %}
{% case 'text' %}
  ... matrixBlock.type is text ...
{% case 'image' %}
  ... matrixBlock.type is image ...
{% default %}
  ... all other cases ...
{% endswitch %}
    

At most one case block is evaluated.

no fall-through, no break

Iterating: for loops


{% for foo in arrayLikeThing %}
  ... do something with {{foo}} ...
{% endfor %}
    

foo is only defined inside the for loop.

array Like Things:

  • any object implementing the Traversable interface. (e.g. ElementCriteriaModel)
  • arrays: ['thingOne', 'thingTwo', 3.14159]
  • sequences (syntatic sugar for arrays): 'a'..'z'
  • associative arrays: { key: value, key2: value2 }

for loop example


{% for animal in ['cat', 'dog', 'fish'] %}
  all about the {{animal}}
{% endfor %}
    

all about the cat
all about the dog
all about the fish
    

Iterating over associative arrays


{% set pets = {
  cat: 'Puff',
  dog: 'Spot'
} %}
    

{% for animal in pets %}
  {{loop.index}}: {{animal}}
{% endfor %}
    

1: Puff
2: Spot
    

Iterating over keys


{% for animal in pets|keys %}
  {{loop.index0}}: {{animal}}
{% endfor %}
    

0: cat
1: dog
    

Iterating over keys and values


{% for key, value in pets %}
  {{key}}: {{value}}
{% endfor %}
    

cat: Puff
dog: Spot
    

Loop variables

  • loop.index and loop.index0
  • loop.first and loop.last
  • loop.revindex and loop.revindex0
  • loop.length
  • loop.parent
    (for example: loop.parent.loop.index)

Functions (macros)

define it:


{% macro foo() %}
  ...
{% endmacro %}
    

macros import it:

defined in the same file


{% import _self as self %}
    

defined in a different file


{% import '_macros/_utils' as m_utils %}
    

macros - call it:


{{ m_utils.foo() }}
    

{% set ourFoo = m_utils.foo() %}
    

Macro richText


{% macro richText(t) %}
  
{# typogrify filter for smart quotes #} {{t|smartypants}}
{% endmacro %}

{{ m_utils.richText(entry.body) }}
    

{{ m_utils.richText() }} {# oops! #}
    

richText continued


{% macro richText(t) %}
  {% if t|default('') %}
    
{{t|smartypants}}
{% endif %} {% endmacro %}

What if we want more classes on that div?


{{ m_utils.richText(entry.body,
                    ['this-class', 'that-class']) }}
    

richText continued


{% macro richText(t, classes=[]) %}
  {% if t|default('') %}
    
{{t|smartypants}}
{% endif %} {% endmacro %}

Moral: deal with possibly missing parameters.

Recursive Macros


{% macro rNav(pages, depth=0) %}
{% import _self as self %}
  <ul>
    {% for page in pages|default([]) %}
      <li>
        {{ page.link }}
        {% if page.hasDescendents() %}
          {{ self.rNav(page.children, depth+1) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{% endmacro %}
    

recursive macro: calling it


{{ m_utils.rNav(craft.entries.section('sitePages')
                             .level(1)) }}
    

Context

In twig, the context is all the values that are currently defined. {% set foo = 'bar' %} — adds foo to the context.

_context is a global variable which represents the current context.


{{ dump(_context|keys) }}
    

array(9) { [0]=> string(5) "craft"
[1]=> string(3) "blx" [2]=> string(3) "now"
[3]=> string(8) "loginUrl"
[4]=> string(9) "logoutUrl"
[5]=> string(8) "siteName" [6]=> string(7) "siteUrl"
[7]=> string(11) "currentUser" [8]=> string(4) "user"
}
     

Macros and _context

_context is not available in macros, but you can pass it in:


{% set meta = {title: 'myTitle',
               description: 'some description'} %}
    

{{ m_meta.output(_context) }}
    

Macros and _context (continued)


{% macro output(c) %}
{% import _self as self %}
  {% if c.meta is defined %} {# in context? #}
    {% set metaData = c.meta %}
  {% elseif c.entry is defined %} {# have entry? #}
    {% set metaData = {
      title: c.entry.mTitle|default(c.entry.title),
      description: c.entry.mDescription
    } %}
  {% else %} {# we got nothing #}
    {% set metaData = {title: '', description: ''} %}
  {% endif %}
  {{self._output(metaData)}}
{% endmacro %}
    

Macros and _context (continued)


{% macro _output(m) %}
  
    {{m.title ? m.title~' | '}}{{siteName}}
  
  {% if m.description %}
    <meta name="description"
          content="{{m.description}}"/>
  {% endif %}
{% endmacro %}
    

Includes


{% for matrixBlock in entry.matrixField %}
  {% include '_blocks/text' %}
{% endfor %}
    

The included template has access to the current context. matrixBlock is defined.

We can be more explicit about this:


{% include '_blocks/text'
   with {currentBlock: matrixBlock} only %}
    

currentBlock will be defined... and nothing else

Includes: smart template names


{% for matrixBlock in entry.matrixField %}
  {% include '_blocks/'~matrixBlock.type %}
{% endfor %}
    

Will include the appropriate template for the block type (assuming you have defined it)


{% for matrixBlock in entry.matrixField %}
  {% include ['_blocks/'~matrixBlock.type,
              '_blocks/_default'] %}
{% endfor %}
    

Will include the template for the block type if it exists, otherwise _blocks/_default.html

Extends


{# this is 'child.html' #}
{% extends '_layouts/base.html' %}

{% set title = entry.title %}
{% block body %}
  {{entry.body}}
{% endblock body %}
    

Since it extends another template, child.html is not allowed to produce any output. It can add things to the context (e.g. title), and define block contents.

Extends (continued)


{# this is '_layouts/base.html' #}
{{title}}
<body>
  {% block body %}hello, world{% endblock body %}
  {% block foot %}Copyright 2015{% endblock foot %}
</body>
    

title will be defined here since it was set in child.html

The body block will be replaced by the body block from child.html

The foot block will be left untouched.

Extends - m_meta again

Another layout template


{% include '_macros/_meta' as m_meta %}
<html>
  <head>
    {{ m_meta.output(_context) }}
  </head>
  <body>
    {% block body %}{% endblock body %}
  </body>
</html>
    

We have abstracted all of the output of the meta data into m_meta.output, which since it has the context, is omniscient.

Embed


...
{% embed '_layouts/left_right.html' %}
  {% block left %}...left content...{% endblock %}
  {% block right %}...right content...{% endblock %}
{% endembed %}
...
    
  • Fine to produce output outside of the embed
  • Like an include, the contents of the embed will be replaced by _layouts/left_right.html
  • Like an embed, the contents of blocks in left_right.html will be replaced by block content given here.

Embed and context

embed provides the same control of the context that include does.


{% embed '_layouts/left_right.html'
   with {foo: 'bar'} %}
  ...
{% endembed %}
    

{% embed '_layouts/left_right.html'
   with {foo: 'bar'} only %}
  ...
{% endembed %}
    

When to use: extends, includes, macros, embeds

  • extends: For layout templates
  • includes: When you need the dynamic template names (always use with and only)
  • macros: All other times. Whenever possible.
  • embeds: Never (so far)
  • But! do what works for you. Strive for consistancy.

Other useful things

dereferencing

foo['bar']

  1. Check if foo is an associative array with key bar
  2. Give up and throw an error

foo.bar or attribute(foo, 'bar')

  1. Check if foo is an associative array with key bar
  2. Check if foo is object with property bar
  3. Check if foo is object with method bar
  4. Check if foo is object with method getBar
  5. Check if foo is object with method isBar
  6. Give up and throw an error

default


foo|default('defaultValue')
        

These are all legal (and will return the default value):

  • neverHeardOfIt|default('')
  • neverHeardOfIt.someField|default('')
  • someArray[invalidIndex]|default('')
  • null|default('')
  • ''|default('')
  • []|default('')
  • false|default('')

0|default('') will not return ''

merge

merge filter merges two arrays (regular or associative)

Use it to add something to an existing array


{% set a = ['this', 'that'] %}
{% set a = a|merge(['theOther']) %}
    

 ['this', 'that', 'theOther']
    

group

group: Groups the items of an array together based on common properties.


{% set books = [
  { author: 'Brown', t: 'Goodnight Moon' },
  { author: 'Burnett', t: 'The Secret Garden' },
  { author: 'Sendak', t: 'The Nutshell Library' },
  { author: 'Seuss', t: 'The Cat in the Hat' }
] %}
{% set alphaBooks = books|group('author|first') %}
    

{ B:[{ author: 'Brown', t: 'Goodnight Moon' },
     { author: 'Burnett', t: 'The Secret Garden' } ],
  S:[{ author: 'Sendak', t: 'The Nutshell Library' },
     { author: 'Seuss', t: 'The Cat in the Hat' } ]
}
    

Thank you!

find me at:

  • marion.newlevant@gmail.com
  • @marionnewlevant
  • http://marion.newlevant.com
  • Marion Newlevant
placekitten