Using "if any():" in Jinja2? - python-3.x

In Jinja2 I'm looking for a way to check if at least one of a list of variables has a value. Basically in python I would do:
if any([item['genre'], item['type'], item['color']]):
However, in Jinja the following isn't valid:
{% if any([item['genre'], item['type'], item['color']]) %}
# some part of the Jinja template
{% endif %}
Is there a way to have the same "any()" check in Jinja2?
For background: the full piece of code that I currently try to add (but isn't valid):
{% if any([item['genre'], item['type'], item['color']]) %}
<ItemProperties>
<ItemProperty key="genre">{{ item['genre'] }}</ItemProperty>
<ItemProperty key="type">{{ item['type'] }}</ItemProperty>
<ItemProperty key="color">{{ item['color'] }}</ItemProperty>
</ItemProperties>
{% endif %}

There is no direct equivalent of the any() function in Jinja2 templates.
For 3 hard-coded elements, I'd just use boolean logic or:
{% if item['genre'] or item['type'] or item['color'] %}
Otherwise, you can use the select() filter without an argument (followed by first() to force iteration). Because select() is itself a generator, using first() on select() makes this short-circuit the way any() would:
{% if (item['genre'], item['type'], item['color'])|select|first %}
Without an argument, select() returns any objects from the input sequence that are true, and first() ensures it iterates no more than needed to find one such element.
The last option is to register a custom filter to just add any() to Jinja yourself; I'd also add all() in that case. You can register both functions directly, since neither takes options:
environment.filters['any'] = any
environment.filters['all'] = all
at which point you can then use
{% if (item['genre'], item['type'], item['color'])|any %}

Related

Check if a variable is defined AND truthy at once in Twig

In PHP I can do
<?php if ($someVar): ?>
This checks if a variable exists and if its value is different from a zero-like value like 0, null, '', and so on.
Is there any way to do so in Twig or do I need to write my own filter for that? At the moment, I must do
{% if someVar is defined and someVar %}
which is a pain when it comes to more complex templates.
There are (at least) two ways of doing this without extending Twig. A third option is to extend Twig by creating e.g. a Twig function. I would probably choose the first way (using the default filter).
By using the default filter
As #The_Unknown pointed out, you can also use the default filter:
{% if someVar|default(null) %}
You can omit passing the default value to the filter, and even omit the parentheses. Then the value will default to an empty string (which is falsey). I.e. these two are equal and valid:
{% if someVar|default() %}
{% if someVar|default %}
Whichever style you choose (default to null, omit the value or omit the parens), stick to it. Be consistent.
See TwigFiddle for a demonstration that truthy values evaluate to true and falsey values evaluate to false (based on the table below).
By setting strict_variables to false
By setting the environment variable strict_variables to false, you can skip the if someVar is defined part and do just {% if someVar %}. As described in Twig's documentation:
strict_variables boolean
If set to false, Twig will silently ignore invalid variables
(variables and or attributes/methods that do not exist) and replace
them with a null value. When set to true, Twig throws an exception
instead (default to false).
Set the variable to false when creating a Twig_Environment instance:
$twig = new Twig_Environment($loader, ['strict_variables' => false]);
If someVar is undefined, then {% if someVar %} is obviously false. The if tag's documentation page describes the edge case rules for defined variables:
The rules to determine if an expression is true or false are the same
as in PHP; here are the edge cases rules:
Value Boolean evaluation
empty string false
numeric zero false
whitespace-only string true
empty array false
null false
non-empty array true
object true
See TwigFiddle for a demonstration (strict_variables is set to false behind the "More options..." link in the header).
By extending Twig
(Disclaimer: I wrote this approach before #The_Unknown pointed out that the default filter can also be used.)
If the idea of setting strict_variables to false is too general, you can also extend Twig. I'd argue that it's better to set strict_variables to true to avoid accidental errors caused by e.g. typos in variable names, so this approach might be better.
I don't think that you can create a filter to do this, as an undefined variable would still throw an exception. You might be able to create a custom tag, test or extension (see Extending Twig for ways to extend Twig); I'm going to create a custom function as it's probably the simplest approach.
$twig->addFunction(new Twig_Function('istruthy', function($context, $var) {
return array_key_exists($var, $context) && $context[$var];
}, ['needs_context' => true]));
The ['needs_context' => true] part is essential here, as then you will have access to $context, which contains the variables present in the current context. (You can e.g. put var_dump($context) above the return statement to see it yourself.)
If you want istruthy to support checking multiple variables at once, you can do this:
$twig->addFunction(new Twig_Function('istruthy', function($context, ...$vars) {
foreach ($vars as $var) {
if (!array_key_exists($var, $context) || !$context[$var]) {
return false;
}
}
return true;
}, ['needs_context' => true]));
Then in Twig you can do:
{% if istruthy('foo') %} ... {% endif %}
{% if istruthy('foo') or istruthy('bar') %} ... {% endif %}
{# These two are the same: #}
{% if istruthy('foo') and istruthy('bar') and istruthy('baz') %} ... {% endif %}
{% if istruthy('foo', 'bar', 'baz') %} ... {% endif %}
{# Ternary operator can also be used: #}
{{ istruthy('foo') ? 'yep' : 'nope' }}
You might want to check in the istruthy function whether the arguments are strings or something else and then act accordingly. array_key_exists expects the first argument to be either a string or an integer.
The following works well:
{% if someVar ?? false %}

How to map scalar twig filter to array

I have a simple array of floats. And I need to show it as comma separated string.
{{ arr|join(', ') }}
is bad solution because of excessive insignificant accuracy.
{% for val in arr %}
{{val|number_format(2)}},
{% endfor %}
is bad because of extra comma at the end.
I would like to do something like this:
{{ arr|map(number_format(3))|join(', ') }}
but I have not found filter map or similar filter it Twig.
Аnd I don't know how to implement such filter.
Why not use the loop variable?
{% for val in arr %}
{{val|number_format(2)}}
{% if not loop.last %}, {% endif %}
{% endfor %}
Quick Answer (TL;DR)
This question relates to higher-order functions
Map is a higher-order function
(see eg) https://en.wikipedia.org/wiki/Map_(higher-order_function)
(see eg) https://en.wikipedia.org/wiki/Higher-order_function
Detailed Answer
Context
Twig 2.x (latest version as of Wed 2017-02-08)
Problem
Scenario: DeveloperGarricSugas wishes to apply higher-order function(s) to a Twig variable
Higher order functions allow any of various transformations on any Twig variable
Twig support for higher-order functions is limited
Nevertheless, there exist addon libraries for this
(see eg) https://github.com/dpolac/twig-lambda
Custom filters or functions can also be used to simulate this
Example
DeveloperGarricSugas starts with a sequentially-indexed array
transform from BEFORE into AFTER (uppercase first letter)
{%- set mylist = ['alpha','bravo','charlie','delta','echo'] -%}
BEFORE:
['alpha','bravo','charlie','delta','echo']
AFTER:
['Alpha','Bravo','Charlie','Delta','Echo']
Solution
{%- set mylist = mylist|map(=> _|capitalize) -%}
Pitfalls
Twig higher-order functions limited support comes from addon-libraries
The above solution does not work with native Twig
See also
https://twigfiddle.com/rsl89m
https://github.com/dpolac/twig-lambda
Quick Answer (TL;DR)
alternate approach is to use Twig loops (workaround)
Workaround
{%- set aaold = [1.234,234.56,11.222,22.333] -%}
{%- set aanew = [] -%}
{% for item in aaold -%}
{{ aanew|merge(item|number_format(3)) }}
{% endfor %}
{{ aanew | join(', ') }}
Pitfalls
requires use of array|merge
alternative to array|merge ... loop.first or loop.last as specified here
requires addition of loop control structure that means extra code bloat

Twig: wrap variable in array if not already one, use empty array if missing

I have a variable modifier passed to a few twig templates. I want to be able to not pass it at all, to pass a single string, or to pass an array of strings. I then want to be able to assume it is always an array of strings (possibly empty) in my template code.
At the moment I have at the start of the twig templates
{% set modifier = modifier | default([]) %}
{% if modifier is not iterable %}
{% set modifier = [modifier] %}
{% endif %}
This does what I want, but is there an easier way? It's a lot of code to do something very simple.
You could one-line it with a filter (can't seem to be able to mimic the default filter though)
<?php
$filter = new Twig_SimpleFilter('wrap_array', function ($value) {
return is_array($value) ? $value : [ $value, ];
});
$twig = new Twig_Environment($loader);
$twig->addFilter($filter);
And use it in your template :
{% set foo = foo|default({})|wrap_array %}

Twig Access Array Index?

Is it possible to directly access an array index from within a Twig template?
Here's my setup, using Silex:
return $app['twig']->render('template', array('numbers' => array('one', 'two', 'three')));
so can I do something like this?
{{numbers[0]}}
Just before posting this I realized, that's exactly what you can do, but as I didn't find the answer anywhere in the docs or google (correct me if I'm wrong), I've posted this anyway.
{{numbers[0]}}
The answer of Adam, is correct, only to make it clear and improve,
you can have access directly to array index
{{ myArray[0] }}
if you need to access in a loop
{% set arrayOfItems = ['ZERO', 'ONE'] %}
{% set myArray = ['APPLE', 'ORANGE'] %}
{% for oneItem in arrayOfItems %}
<p>{{ oneItem }} equals {{ myArray[loop.index0] }}</p>
{% endfor %}
in this example I used an array inside a non related loop so the result is:
ZERO equals APPLE
ONE equals ORANGE
Thats actually something what doesnt work for me when using Twig with shopware 6.
I try to access an object like
{{ page.cart.lineItems.elements[0].quantity }}
what will lead into a parsing error of the Twig Template
I can use
{{ page.cart.lineItems.elements | first }}
to get the first Element, but dont know how i can then access a property of this first element

Does Twig allow assignments in conditions and how?

As per title, does Twig allow assignments in conditional and how? I know that not all people like these assignments but sometimes they can be very helpful.
This:
{% if (name = attribute(mappings, property)) is defined %}
{% else %}
{% set attrs = attrs|merge(['%s="%s"'|format(name, value)]) %}
{% endif %}
...is not working and gives me and error:
An opened parenthesis is not properly closed. Unexpected token
"operator" of value "=" ("punctuation" expected with value ")") in
::tooltips.html.twig at line 29.
Your twig error is because of this line
{% if (name = attribute(mappings, property)) is defined %}
Twig doesn't like assignment-in-condition expressions, and personally neither do I, it's just a lazy shortcut and the potential issues isn't worth saving a few keystrokes.
But I have to now admit that I'm confused by what this is supposed to do. You're attempting to capture the result of attribute(mappings, property) into name but if that fails, only then do you do something using name, but by then name won't have a value unless it's been defined elsewhere in the template.

Resources