Twig3: How to migrate "for item in items if item.foo == 'bar'" with loop bariable - symfony

I use the following Twig 2 code:
{% for item in items if item.foo == 'bar' %}
<span class="{% if loop.index % 2 %}special-class{% endif %}">
{{ item.value }}
</span>
{% else %}
Nothing found
{% endfor %}
In the twig docs: https://twig.symfony.com/doc/2.x/deprecated.html
Adding an if condition on a for tag is deprecated in Twig 2.10. Use a filter filter or an "if" condition inside the "for" body instead (if your condition depends on a variable updated inside the loop)
I wonder how I migrate my Twig 2 code to Twig 3. As you see I use the loop variable and else in the for loop. I know that I can use a new parameter and increase it myself... but it that really the intention? How do I rewrite this code using filter?

You have two options to solve this
Place the if-tag inside the loop
{% set i = 0 %}
{% for item in items %}
{% if item.foo == 'foo' %}
<span class="{% if i % 2 %}special-class{% endif %}">
{{ item.value }}
</span>
{% set i = i + 1 %}
{% endif %}
{% else %}
Nothing found
{% endfor %}
With this solution you can't "rely" on the internal loop variable, as the counter keeps going up whether or not the condition was met
Use the filter - filter
{% for item in items | filter(item => item.foo == 'foo') %}
<span class="{% if loop.index % 2 %}special-class{% endif %}">
{{ item.value }}
</span>
{% else %}
Nothing found
{% endfor %}
updated demo

Using the filter filter your code would look something like this (see also https://twigfiddle.com/9hiayc and https://twigfiddle.com/9hiayc/2):
{% for item in items|filter(i => i.foo == 'bar') %}
<span class="{% if loop.index % 2 %}special-class{% endif %}">
{{ item.value }}
</span>
{% else %}
Nothing found
{% endfor %}

Related

How to change "if" condition related to a page

I would like to include tab_system.twig file in two differents files : archive.twig and blog_list.twig to have the same html in those two files.
{% include "tab_system.twig" %}
In this tab_system.twig file I have a condition to add the active class :
{% if XXX %}active{% endif %}
But this condition must be different depending on which page the user is.
For blog_list.twig :
{% if loop.first %}active{% endif %}
For archive.twig :
{% if item.term_id == term_page.term_id %}active{% endif %}
I wrote this without success :
{% set addclass = '' %}
{% if is_blog_list %}
{% set addclass = '{% if loop.first %}active{% endif %}' %}
{% elseif is_archive %}
{% set addclass = '{% if item.term_id == term_page.term_id %}active{% endif %}' %}
{% endif %}
In tab_system.twig I have a tab system with menu from a part and content to the other part. I wrote a js loop to display the corresponding content. I need to add active class on the first link and first content tab in blog_list.twig file and to add active class to the link and content tab depending on which category the user is.
<div class="tab-menu-list">
{% for item in all_posts %}
<a href="#" class="tab-menu {{ addclass }}"</a>
{% endfor %}
</div>
<div class="tab-content-list">
{% for item in all_posts %}
<div href="#" class="tab-content {{ addclass }}"</div>
{% endfor %}
</div>
is_archive and is_blog_list are variables defined elsewhere. They work
How can I create a condition ? Thank you in advance.
As I'm assuming you are looping the records, I'd say don't overcomplicate things,
single.twig
{% for i in 1..5 %}
{% include 'article.twig' with { 'active' : loop.first, } %}
{% endfor %}
template_blog.twig
{% for item in items %}
{% include 'article.twig' with { 'active' : item.term_id == term_page.term_id, } %}
{% endfor %}
article.twig
<div class="articles__list tab-content{{ active ? ' active'}}">
foo
</div>
demo - just change the main template
If you really wanted to keep that for inside the article template, which I think is not a great idea, you could still use the variables is_archive and is_blog_list inside article, e.g.
article.twig
{% for item in items %}
{% if is_archive %}
{% set active = loop.first %}
{% elseif is_blog_list %}
{% set active = item.term_id == term_page.term_id %}
{% endif %}
<div class="articles__list tab-content{{ active ? ' active'}}">
{% endfor %}

Twig use one value in different for loop

Since I'm using a SaaS platform I don't have much space to do things differently.
I have two for loops in Twig:
{% for option in product.options %}
{{ option.title }}
{% if option.values %}
{% for value in option.values %}
{{ value.title }}
{% endfor %}
{% endif %}
{% endfor %}
{% for variant in product.variants %}
{{ variant.stock.level }}
{% endfor %}
What I try to do is to use variant.stock.level value inside the product.options for loop to show some HTML. This value always match the corresponding index value of the other for loop. I also think that's the only way to do this.
So what I mean is.....Let's say both for loops contain 3 elements.
Option1
Option2
Option3
Variant1
Variant2
Variant3
So option1 needs to have the value from variant1.
For the end result I need to know what the value of eg Variant1 is to show some HTML like so:
{% for value in option.values %}
{% check if value from corresponding variant is greater then 0 %}
<li class="on-stock">{{ value.title }}</li>
{% else %}
<li class="out-of-stock">{{ value.title }}</li>
{% endif %}
{% endfor %}
I don't know no other way to explain this :) Any help appreciated....
You are looking for attribute.
The attribute function can be used to access a "dynamic" attribute of a variable
Since the indexes are the same, you can use loop.index0
Example for your case
{% if attribute(option.variants, loop.index0) > 0 %}
// some stuff
{% endif %}
I'm not sure if I understood you correctly but you may use key from first loop to access product.variants with the same index.
{% for key, option in product.options %}
{{ option.title }}
{% if option.values %}
{% for value in option.values %}
{{ value.title }}
{% endfor %}
{% endif %}
{{ product.variants[key].stock.level }}
{% endfor %}

Using a counter in a Twig node template

Setting up a Drupal 8 custom view for Top 4 items, I want a different layout for item 1 than the remaining. I have override files for the custom view, but for this example I'm using base files to keep it simple.
In views-view.html.twig base file we have:
<div class="view-content">
{{ rows }}
</div>
In node.html.twig base file we have:
<div {{ content_attributes.addClass('content') }}>
{{ content }}
</div>
In node.html.twig I am aiming for something like:
{% if row_counter = 1 %}
output this markup / fields
{% else %}
do something boring with the other 3 items.
{% endif %}
I was able to set row_counter in the views-view.twig.html file:
{% for row in rows %}
{% set row_counter = loop.index %}
<div{{ row.attributes }}>
{{ row_counter }}
</div>
{% endfor %}
But I need to check against the value of {{ row_counter }} in the node.html.twig file....
What other properties are available in node.html.twig to check against its position in the list?
From the documentation
The loop variable
Inside of a for loop block you can access some special variables:
Variable Description
-----------------------------------------------------------------
loop.index The current iteration of the loop. (1 indexed)
loop.index0 The current iteration of the loop. (0 indexed)
loop.revindex The number of iterations from the end of the loop (1 indexed)
loop.revindex0 The number of iterations from the end of the loop (0 indexed)
loop.first True if first iteration
loop.last True if last iteration
loop.length The number of items in the sequence
loop.parent The parent context
edit: every variable know in the parent template is also known inside an include as context is passed by default. Only macro's don't know the parent's context
controller
<?php
require __DIR__ . '/../requires/propel_standalone.php';
echo $twig->render('tests/items.html', [
'items' => [
'Abc',
'def',
'ghi',
'jkl',
'mno',
'pqr',
'stu',
'vwx',
'z',
],
]);
items.twig
<!doctype>
<html>
<head><title>Test</title></head>
<body>
{% for item in items %}
{% include "tests/item.html" %}
{% endfor %}
</body>
</html>
item.twig
{% set order = 'Nothing to report' %}
{% if loop.first %}
{% set order = 'I\'m first' %}
{% endif %}
{% if loop.last %}
{% set order = 'I\'m last' %}
{% endif %}
{% if loop.index is even %}
{% set order = 'I\'m even' %}
{% endif %}
{% if loop.index is divisible by(5) %}
{% set order = 'I can be dived by 5' %}
{% endif %}
{% if loop.index is divisible by(3) %}
{% set order = 'I can be dived by 3' %}
{% endif %}
<div>
<b>{{ loop.index }}:</b>{{ order }} - {{ item }}
</div>
You could do something like this if a class is enough:
Your views template:
{% for row in rows %}
<div class="{% if loop.index == 1 %}first_element{% endif %}">
{{ row.content }}
</div>
{% endfor %}
And then just style it appropriately.

Twig variable from one loop in another doesn't work

I have two loops in Twig. One is to check the stock level of a product, the other is to display some product options. I'm trying to create a variable from one loop which I can use inside the other to add a class name to a list(s).
I can't get that to work. Any help is more then welcome...
What I have is this:
{% set stockLevel = '' %}
{% for variant in product.variants %}
{% set stockLevel = variant.stock.level %}
{{ stockLevel }} // gives 1 0 1 1 (So all sizes except the second one are available)
{% endfor %}
{% for option in product.options %}
<ul class="optionslist">
{% if option.values %}
{% for value in option.values %}
<li class=" {% if stockLevel == 0 %}not_on_stock {% endif %}" >
<a class="item">etc...</a>
</li>
{% endfor %}
{% endif %}
</ul>
{% endfor %}
I believe there is something wrong the way you are handling your data. Right now, event if your variable was accessible in your second for loop, the value of it would be the one set lastly and your script would've failed anyway.
I can suggest a bit of a hacky way, feel free to improvise or use the idea behind it.
Say, we assign an empty array:
{% set variantsArray = [] %}
Then, we will use the filter merge to fill it with some dummy data(that's where your first loop comes into play)
{% for number in 1..5 %}
{% set variantsArray = variantsArray | merge([number]) %}
{% endfor %}
Then to be sure, we can access all the values there, we will just dump it out(that's where your second loop comes)
{% for index in 1..3 %}
{{ dump(variantsArray) }}
{% endfor %}
The following dump generated:
array:5 [▼
0 => 1
1 => 2
2 => 3
3 => 4
4 => 5
]
Hope you got the idea. Any suggestions are kindly welcome.
I am not sure what you are trying to do, how do you get your stock level from the variants? i think it is better to have a service function which help you get your stock level by product, the code would be cleaner :
controller action
$stock_helper = $this->container->get('stockhelper_service');
$stocklevel = $stock_helper->getStockLevelByProduct($product); //getstocklevelbyproduct handle your logic and return the stock level integer.
pass this variable to the twig.
One variable in your twig, no loops there, clean view, understandable code in your view and action.
Ok, I found the solution. The trick is to "connect" both values. Like so:
{% for option in product.options %}
{% if option.values %}
<ul id="product_configure_option2_{{ option.id }}" data-id="{{ option.id }}">
{% for value in option.values %}
{% set stock = null %}
{% for variant in product.variants **if variant.title == (option.title ~ ': ' ~ value.title)** %}
{% set stock = variant.stock %}
{% endfor %}
<li value="{{ value.id }}"{% if not stock.available %} class="not_on_stock"{% endif %}>{{ value.title }}</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}

For loop - iterate from 1 to x

I have loop in Twig template:
{% for item in 1..0 %}
{{ item }}
{% endfor %}
(of course in real life instead of 0 there is variable value). I would expect in this example that loop won't be executed because 0 is lower than 1. But in fact it displays
1 0
Question: is there any way in this example using simple for loop in Twig (or other loop) to tell Twig I want to iterate ++ not -- or I have to add condition to check before loop if 0 is lower than 1
EDIT:
Of course I'm aware I can create array in PHP and use it in Twig template but what about this case - without creating array in PHP
EDIT2:
I want it simple use for generating star rating. Now I need to have code because I had to add extra if checking for each for loop:
{% if full_stars_number >= 1 %}
{% for item in 1..full_stars_number %}
<img src="img/full_star.png" />
{% endfor %}
{% endif %}
{% if half_stars_number >= 1 %}
{% for item in 1..half_stars_number %}
<img src="img/half_star.png" />
{% endfor %}
{% endif %}
{% if empty_stars_number >= 1 %}
{% for item in 1..empty_stars_number %}
<img src="img/empty_star.png" />
{% endfor %}
{% endif %}
Try this:
{% for item in 1..full_stars_number if full_stars_number>0 %}
<img src="img/full_star.png" />
{% endfor %}
{% for item in 1..half_stars_number if half_stars_number>0 %}
<img src="img/half_star.png" />
{% endfor %}
{% for item in 1..empty_stars_number if empty_stars_number>0 %}
<img src="img/empty_star.png" />
{% endfor %}
This is basically the same as what you already did in your EDIT2, but in a slightly more concise way.
1..x generates a collection, with inclusive values. It can be ordered ascending or descending. This is expected.
I'm not sure what you are trying to achieve, but the way you described it is not possible in twig out of the box.
You could write your own twig function, which would generate the values, or return an empty array:
{% set itemsCount = 0 %}
{% for item in my_crazy_function(itemsCount) %}
{{ item }}
{% endfor %}
Edit:
If you simply want to repeat a value number of times you could write a twig filter:
{{ '<img src="img/empty_star.png" />' | repeat(5) }}
A function implementation could just be a call to PHP's str_repeat:
class Project_Twig_Extension extends Twig_Extension
{
public function getFilters()
{
return array(
new Twig_SimpleFilter('repeat', 'str_repeat'),
);
}
// ...
}

Resources