list, else, items, sep, break, continue
Synopsis
The simplest form for listing a sequence* (or collection) is:
and to list the key-value pairs of a hash:
But these are just special cases of the generic forms, which are shown below. Note that for simplicity we only show the generic forms for sequence listing; simply replace “as item
” with “as key, value
” to get the generic form for hash listing.
Generic form 1:
Where:
- The
else
part is optional, and is only supported since FreeMarker 2.3.23. - sequence: Expressions evaluates to a sequence or collection of the items we want to iterate through
- item: Name of the loop variable (not an expression)
- The various “parts” between the tags can contain arbitrary FTL (including nested
list
-s)
Generic form 2:
Where: see the “Where” section of Form 1 above (and thus the else
part is optional here too).
Description
Simplest form
Assuming users
contains the ['Joe', 'Kate', 'Fred']
sequence:
The list
directive executes the code between the list
start-tag and list
end-tag (the body of list
from now on) for each value in the sequence (or collection) specified as its first parameter. For each such iteration the loop variable (user
in this example) will store the value of the current item.
The loop variable (user
) only exists inside the list
body. Also, macros/functions called from within the loop won’t see it (as if it were a local variable).
Listing hashes is very similar, but you need to provide two variable names after the as
; one for the hash key, and another for the associated value. Assuming products
is { "apple": 5, "banana": 10, "kiwi": 15 }
:
Note that not all hash variables can be listed, because some of them isn’t able to enumerate its keys.
else directive
The else
directive is used if when there are 0 items, you have to print something special instead of just printing nothing:
This outputs the same as the earlier example, except when users contains 0 items:
Note that the loop variable (user
) doesn’t exist between the else
tag and the list
end-tag, since that part is not part of the loop.
else
must be literally (means, in the source code) inside the body of the list
directive. That is, you can’t moved it out into a macro or included template.
items directive
The items
directive is used if you have to print (or do) something before the first list item, and after the last list item, as far as there’s at least 1 item. A typical example:
If there are 0 items, the above won’t print anything, thus you don’t end up with an empty <ul></ul>
.
That is, when the list
directive has no as item
parameter, the body of its is executed exactly once if there’s at least one item, or not at all otherwise. It’s the body of the mandatory nested items
directive that will be run for each item, and hence it’s also the items
directive that defines the loop variable with as item
, not list
.
A list
directive with items
also can have an else
directive:
Some further details:
-
The parser will check that a
list
withoutas item
parameter always has a nesteditems
directive, and that anitems
directive always has an enclosinglist
which has noas item
parameter. This is checked when the template is parsed, not when the template is executed. Thus, these rules apply on the FTL source code itself, so you can’t moveitems
out into a macro or included template. -
A
list
can have multipleitems
directives, but only one of them will be allowed to run (as far as you don’t leave and re-enter the enclosinglist
directive); and further attempts to callitems
will cause error. So multipleitems
can be utilized on differentif
-else
branches for example, but not for iterating twice. -
items
directive can’t have its own nestedelse
directive, only the enclosinglist
can have -
The loop variable (
user
) only exists inside the body of theitems
directive.
sep directive
sep
is used when you have to display something between each item (but not before the first item or after the last item). For example:
Above, <#sep>, </#list> is a shorthand for <#sep>, </#sep></#list>; the sep end-tag can be omitted if you would put it where the enclosing directive is closed anyway. In the next example, you couldn’t use such abbreviation (HTML tags close nothing, as they are just raw text to output for FreeMarker):
sep
is just a shorthand for <#if item?has_next>...</#if>
. Thus, it can be used anywhere where there’s a list
or items
loop variable available, it can occur for multiple times, and it can have arbitrary nested content.
The parser ensures that sep
is only used on a place where there’s a visible loop variable. This happens earlier than the actual execution of the template. Thus, you can’t move sep
from inside the associated list
or items
directive into a macro or included template (the parser can’t know where those will be called from).
break directive
break is deprecated for most use cases, as it doesn’t work well with
<#sep>
anditem?has_next
. Instead, use sequence?take_while(predicate) to cut the sequence before you list it. See also examples here.
You can exit the iteration at any point with the break
directive. For example:
The break
directives can be placed anywhere inside list
as far as it has as item
parameter, otherwise it can be placed anywhere inside the items
directive. However, it’s strongly recommended to place it either before or after all the other things that you do inside the iteration. Otherwise it’s easy to end up with unclosed elements in the output, or otherwise make the template harder to understand. Especially, avoid breaking out from the nested content of custom directives (like <#list ...>...<@foo>...<#break>...</@foo>...</#list>
), as the author of the directive may not expect that the closing tag (</@foo>
) is never executed.
If the break
is inside items
, it will only exit from items
, not from list
. In general, break
will only exit from the directive whose body is called for each item, and can only be placed inside such directive. So for example can’t use break
inside list
’s else
section, unless there’s the list
is nested into another break
-able directive.
Using break
together with sep
or ?has_next
is generally a bad idea, as these can’t know if you will skip the rest of items with a break
. To solve such situations see these examples.
Just like else
and items
, break
must be literally inside body of the directive to break out from, and can’t be moved out into a macro or included template.
continue directive
continue is deprecated for most use cases, as it doesn’t work well with
<#sep>
,item?has_next
,item?counter
,item?index
,item?item_parity
, etc. Instead, use sequence?filter(predicate) to remove unwanted elements. See also examples here.
You can skip the rest of the iteration body (the section until the </#list>
or </#items>
tag) with the continue
directive, then FreeMarker will continue with the next item. For example:
The continue
directives can be placed anywhere inside list
as far as it has as item
parameter, otherwise it can be placed anywhere inside the items
directive. However, it’s strongly recommended to place it before all the other things you do inside the iteration. Otherwise it’s easy to end up with unclosed elements in the output, or otherwise make the template harder to understand. Especially, avoid breaking out from the nested content of custom directives (like <#list ...>...<@foo>...<#continue>...</@foo>...</#list>
), as the author of the directive may not expect that the closing tag (</@foo>
) is never executed.
When you call continue
, the sep directive will not be executed for that iteration. Using continue
together with sep is generally a bad idea anyway, also ?has_next
, ?counter
, ?index
, ?item_parity
, etc. will not work as you certainly wanted if you completely skip items. To solve such situations see these examples.
Just like break
, continue
must be literally inside body of the directive whose iteration need to be “continued”, and can’t be moved out into a macro or included template.
Accessing iteration state
Loop variable built-ins is the preferred way of accessing current state of the iteration. For example, here we use the counter
and item_parity
loop variable built-ins (see all of them in the Reference):
Skipping items conditionally
If you need to skip certain element in a list, it’s generally a bad idea to use if directive for that, because then <#sep>
, item?has_next
, item?counter
, item?index
, item?item_parity
, etc., will not be usable, as FreeMarker doesn’t know what items were and will be actually displayed. Instead, you should try to remove the unwanted items from the sequence that you will list, and then list it. Here are some typical examples with and without if
.
Filtering
In this example, you want to show the recommended products from products
. Here’s the wrong solution with if
:
Here’s the good solution that uses the [filter
built-in]:
Stop listing when a certain element is found
Let’s say you have a list of lines in lines, and you need to stop at the first empty line (if there’s any). Furthermore you need to
between the elements. Here’s the wrong solution with if and break:
Here’s the good solution that uses the take_while built-in (note that the condition is inverted compared to the if+break solution):
Nesting loops into each other
Naturally, list
or items
can contain further list
-s:
It’s also allowed to use clashing loop variable names like:
Treatment of missing (null) elements
As you know by now, the list
directive will repeat its nested content for each element of the listed value. However, it’s technically possible that you have holes (missing values - null
-s) in the list of elements. The nested content will executed for these “holes” as well, but then the loop variable (the variable whose name you specify after the as
keyword) will be missing. When FreeMarker finds that there’s no variable with the given name in the loop variable scope, it will just fall back to a higher scopes to find it. However, this fallback behavior can be problematic in this case. Consider:
Here, the intent of the author is to print “Missing” for the missing elements (hopes) of xs
. But if accidentally there’s an x
variable in a higher scope, like in the data-model, then x
will evaluate to that in case the currently listed element is missing, and so it won’t default to “Missing”.