Fluid: Escaping nested Inline ViewHelper

While working with TYPO3’s template engine Fluid it is sometimes necessary (or more elegant) to nest ViewHelpers for directing some function’s return value to the next function, e.g. for formatting.

TL;DR: If all you need is taking a look at a working example, please scroll down to the last code snippet of this article.

Now, let’s start a few steps back.

What is Fluid’s Inline Notation for ViewHelpers and when should you use it?

ViewHelpers are nifty little functions for your Fluid Template helping you to render data into your HTML offering if-then-else-comparisons, localising labels, counting, formatting, debugging, and a lot more. Many of them already come with your TYPO3 Core.

Then can be as simple as in this example where the output simply will be how many items are inside of {sliderElements}.

<!-- all lines will output the same value, 
    they are only alternative ways of calling 
    this ViewHelper (count) -->
<f:count>{sliderElements}</f:count>
{f:count(subject: sliderElements)}
{sliderElements -> f:count()}

Why should I use Inline Notation at all?

Now imagine you want to render a sweet JavaScript element slider, but only if there are at least two items to slide available. While this will actually work, please don’t do it like this:

<f:if condition="<f:count>{sliderElements}</f:count> > 1">
  <!-- slider html code here -->
</f:if>

Technically that’s a tag inside another tag and therefore not valid. Additionally it’s not nice to read. That’s where inline notation comes in handy. Inside the if-condition you can easily read which object matters (sliderElements) and what function you’re using on it (count()).
Bonus: it’s a tiny bit shorter (yay!)

<f:if condition="{sliderElements -> count()} > 1">

Okay, but why and when do I want to nest them?

In short you need to nest your Fluid ViewHelpers when you want something to be done with the output of another function that also took its input from some other function.

As shown in the following example where the following happens: The floating-point number {cheapestPrice} is formatted with f:format.currency() using localised parameters from f:translate() (sepThousands and sepDecimals).

(10013 Dollars and 42 Cents look nicer written as $ 10,013.42, but e.g. in Germany it is written as $ 10.013,42. That’s why the separators are stored within the translation files.)

{cheapestPrice -> f:format.currency(
  thousandsSeparator: '{f:translate(key:\'sepThousands\')}',
  decimalSeparator: '{f:translate(key:\'sepDecimals\')}',
  decimals: 2
)}
So what’s with the escaping then?

In the example above the function f:format.currency() is given three parameters, written as parameterName : 'parameterValue'. Now the parameterValue is a function call itself with its own parameters. Fluid will choke on all those single quotation marks since it can not differ where one parameter ends and another function inside it is called. That’s why we need to escape matching quotation marks with backslashes starting with the second level of nesting (which is f:translate() in the example above).
While nesting deeper, the needed amount of escaping backslashes increases:

  • 1st level: 0 '
  • 2nd level: 1 \'
  • 3rd level: 3 \\\'
  • 4th level: 7 \\\\\\\'
  • 5th lev— wait… are you sure you need to nest five or more levels of ViewHelpers in your code? Is this even readable anymore? Please go ahead and do the math yourself, but reconsider rewriting that whole block of Fluid code. If not for you, do it for your workmate who has to fix something in 2 years while you are on a vacation.

So to wrap things up and just so you can say you already saw a four level deep nesting of ViewHelpers, have a look at this snippet taken from the backend logging inside a TYPO3 Core file:

<f:comment>
  Nest view helpers three times:
  1. Feed pid as argument to be.pagePath
  2. Use this as argument for 'forPage' translate
  3. Use this as argument for 'logForNonPageRelatedActionsOrRootLevelOrPage' translate
</f:comment>
<f:translate
  key="logForNonPageRelatedActionsOrRootLevelOrPage"
  arguments="{ 
    0: '{f:translate( 
      key:\'forPage\', 
      htmlEscape:\'0\', 
      arguments:\'{
        0:\\\'{belog:be.pagePath(pid:\\\\\\\'{pid}\\\\\\\')}\\\', 
        1:\\\'{pid}\\\' 
      }\' 
    )}', 
    1: '{f:format.date(format:\'{settings.dateFormat} H:i\', date:\'@{constraint.startTimestamp}\')}', 
    2: '{f:format.date(format:\'{settings.dateFormat} H:i\', date:\'@{constraint.endTimestamp}\')}' 
  }" 
/>