A solution for grouping items in loop with Timber and Twig - twig

Often i need to do some tricky layout on dynamic elements like galleries.
Here's one example:
<ul>
<li class="slide">
<img src="img_01.jpg">
<img src="img_02.jpg">
</li>
<li class="slide">
<img src="img_03.jpg">
<img src="img_04.jpg">
</li>
<li class="slide">
<img src="img_05.jpg">
<img src="img_06.jpg">
</li>
</ul>
I've managed to do it with the following snippet. But i wanted some suggestions if possible about how to make it more flexible or more simple, like grouping by any number. Maybe using cycle() or any other method. I was getting strange results using the slice() or array[1:2] notation.
<ul>
{% for image in gallery %}
{% set current = loop.index %}
{% set next = current + 1 %}
{% if current is odd %}
<li class="slide">
{% for image in gallery %}
{% if loop.index in [current,next] %}
{% set th = TimberImage(image) %}
<img src="{{th.src}}">
{% endif %}
{% endfor %}
</li>
{% endif %}
{% endfor %}
</ul>
Any suggestions are welcomed.
Timber becomes very handy for quick in and out fixes with Timber::compile or custom themes with full routing. The purpose of the question is to create some snippet that can be reused.
Kudos to creators.
https://github.com/timber/timber

You can approach with the rest of the division with the following code (Here a working solutions):
{# number of element for every section #}
{% set section = 2%}
<ul>
{% for image in gallery %}
{% if loop.index % section == 1 %}
<li class="slide">
{% endif %}
{% set th = TimberImage(image) %}
<img src="{{th.src}}">
{% if loop.index % section == 0 or loop.last %}
</li>
{% endif %}
{% endfor %}
</ul>
You can easily reuse this code making a Twig macro using as parameter the gallery and the number of element for section (highlighted with the variable section

Here's the final result taking the suggestion of #Matteo for a macro:
https://gist.github.com/lithiumlab/5ee0454b0a77b1cc26fc0ce8ba52fd80
views/single.twig:
{% import 'utils.twig' as utils %}
{{utils.group_collection(gallery,3)}}
views/utils.twig:
{% macro group_collection(collection, groupby) %}
{% set section = groupby|default(2) %}
<ul>
{% for element in collection %}
{% if loop.index % section == 1 %}
<li class="group">
{% endif %}
{% set th = TimberImage(element) %}
<img src="{{th.src}}">
{% if loop.index % section == 0 or loop.last %}
</li>
{% endif %}
{% endfor %}
</ul>
{% endmacro %}

Related

How to manipulate count based on sub-values in TWIG?

I was wondering how to manipulate / render the count based on sub-rules.
In the current situation specifications are counted and when there are more than 5, a link is shown to show more specifications. I wanted to show only specs that have a value like this:
{% for spec in product.specs | limit(5) %}
{% if spec.value %}
<li>
<span>{{ spec.title }}</span>
{{ spec.value }}
</li>
{% endif %}
{% endfor %}
{% if product.specs | length > 5 %}
<li class="more">{{ 'View all specifications' | t }}</li>
{% endif %}
In this case the count is done before checking the values of the sub-items, so the "Show more" link is visible and clickable, but doesn't show more specifications, because they are stripped out, because the value is empty but is counted as item.
The goal is to hide the "Show more" link when there are < 5 items WITH values.
I hope anyone could point me in the right direction :-)
Thank you very much for thinking with me!
You could either use an extra counter to count the valid elements,
{% for spec in product.specs | limit(5) %}
{% if spec.value %}
<li>
<span>{{ spec.title }}</span>
{{ spec.value }}
</li>
{% endif %}
{% endfor %}
{% set cnt = 0 %}
{% for spec in product.specs %}
{% if spec.value %}{% set cnt = cnt + 1 %}{% endif %}
{% endfor %}
{% if cnt >= 5 %}
<li class="more">{{ 'View all specifications' }}</li>
{% endif %}
Or you can use the filter filter
{% if products.specs| filter(spec => spec.value|default) | length >= 5 %}
<li class="more">{{ 'View all specifications' }}</li>
{% endif %}
Update as for your comment (and as you don't have access to filter)
You can't just limit the result before hand. So in the first part you would also need to use a counter
{% set cnt = 0 %}
{% for spec in product.specs %}
{% if cnt < 5 %}
{% if spec.value %}
<li>
<span>{{ spec.title }}</span>
{{ spec.value }}
</li>
{% set cnt = cnt + 1 %}
{% endif %}
{% endif %}
{% endfor %}

Is there a way to programmatically have 4 cards per row?

I am currently working with Python Flask and Bootstrap.
I am looking to get it so that if there are 4 cards in one row, it will automatically create a new row.
Issue i am having just now is the more posts I have the longer and thinner the rows cards get.
Current code:
{% extends "base.html" %}
{% block content %}
<div class="card-deck">
{# Go through each forum post #}
{% for post in forum_posts.items %}
<div class="card">
<div class="card-body">
<span class="badge badge-info">{{ post.cat }}</span>
<h3 class="card-title"><a class="card-title"
href="{{ url_for('forum_posts.view_post', forum_post_id=post.id) }}">{{ post.title }}</a>
</h3>
<h6 class="card-subtitle mb-2 text-muted">Written by: {{ post.author.username }}</h6>
<p>{{ post.text[:100] }}...</p>
</div>
<div class="card-footer">
<a href="{{ url_for('forum_posts.view_post', forum_post_id=post.id) }}"
class="btn btn-primary">Read
Blog Post</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
You could code this on the python side so that it makes it easier in jinja2, using an array of arrays:
>>> arr = [0,1,2,3,4,.....,102]
>>> forum_posts.items = []
>>> for i in range(int(len(arr)/4)):
j = i * 4
forum_posts.items.append([arr[j], arr[j+1], arr[j+2], arr[j+3]])
# needs an error trap for IndexError on 103
Then in jinja2: you can double loop:
{% for row in forum_posts.items %}
{% for item in row %}
{# HTML here for a new row of at most 4 cards #}
There is a way of doing the same kind of thing in jinja2 using variable loop counters and setting new row HTML if for example the new loop index is an exact multiple of 4:
{% for i, post in enumerate(forum_posts.items) %}
{% if i % 4 == 0 %}
{# new row code #}
{% else %}
{# regular row code %}
{% endif %}
{% endfor %}

How to chop inside a for-loop in Twig

This is a refactoring question. The code works as is, I'm just not happy with it in an aesthetical sense.
I would like to know if the conditional inside the loop can be written in a shorter, more readable way or maybe can be stripped away?
{% set i = 0 %}
{% for element in list %}
{% if loop.first %}<div class="row">{% endif %} {# open first row #}
{% if i > 2 %} {# new row every 3 elements #}
{% set i = 0 %}
</div>
<div class="row">
<img src="{{ element.url }}">
{% else %}
{% set i = i+1 %}
<img src="{{ element.url }}">
{% endif %}
{% if loop.last %}</div>{% endif %}
{% endfor %}
As user DarkBee said, have a look into batch.
{% for element in list|batch(3) %}
.....
.....
{% endfor %}
Just to have an example on this page.
Batch-Docs
Regards

Use loop.index as part of template name to include with Twig?

I have an array of 3 items so in the following I'm including sub-component.twig 3 times:
{% for i in array %}
<div class="my-class">
{% include "sub-component.twig" %}
</div>
{% endfor %}
However I actually have 3 slightly different templates and I would like to load a different one for each iteration over the array:
sub-component-1.twig
sub-component-2.twig
sub-component-3.twig
When I print loop.index in the template the result is "1", "2" and "3". Can I therefore use the index to form the template name?
{% for i in array %}
<div class="my-class">
{{ loop.index }}
{% include ["sub-component-" ~ loop.index ~ ".twig"] %}
</div>
Possibly because I'm using gulp twig I had to break things out into variables for this to work.
https://github.com/zimmen/gulp-twig
{% for i in array %}
<div class="my-class">
{% set sub-component_1 = "sub-component-" %}
{% set sub-component_2 = loop.index %}
{% set sub-component_3 = ".twig" %}
{% set sub-component_full = sub-component_1 ~ sub-component_2 ~ sub-component_3 %}
{% include sub-component_full %}
</div>
{% endfor %}

Liquid and Arithmetic

I am working on some pagination and I am wondering if there is a way to tell liquid to only show 5 pages. The output I am looking for is
<< First 5 6 7 8 9 Last >>
The logic I currently have in place works but it is showing all 30 some pages.
{% for count in (2..paginator.total_pages) %}
{% if count == paginator.page %}
<span class="current">{{ count }}</span>
{% else %}
{{ count }}
{% endif %}
{% endfor %}
I would like to be able to make the 2 and paginator.total_pages be dynamic, I have tried
{% for count in ((paginator.page - 2)..(paginator.page + 2)) %}
This code however does not actually do the math, if paginator.page = 5 then the loop is 5..5 and does not provide the expected results. I can figure out the logic so that it does not hit negative numbers and works as expected but how can I do math equations in this?
You need use a filter on paginator.total_pages to do the arithmetic, and then capture the result in a variable using the capture tag. Once you have the start and end pages, you can write the for loop as you normally would:
{% capture page_start %}{{ paginator.page | minus: 2 }}{% endcapture %}
{% capture page_end %}{{ paginator.page | plus: 2 }}{% endcapture %}
{% for count in (page_start..page_end) %}
{% comment %} ... do your thing ... {% endcomment %}
{% endfor %}
I'm building a blog with Jekyll and I've faced a similar situation. According to what I've found in the Liquid wiki is possible to iterate over a subset of a given collection using limit and offset.
The following example reflects your particular case and should work correctly in every page, from the first to the last one:
{% capture start %}{{ paginator.page | minus: 3 }}{% endcapture %}
{% for i in (1..paginator.total_pages) limit: 5 offset: start %}
...
{% endfor %}
I am using Bootstrap 3.0.3 for my website. I use the following code for pagination.
It has the same effect you're looking for. The code I posted above is what you're looking for, but I will post my bootstrap pagination code here anyways.
{% if paginator.total_pages != 1 %}
{% if paginator.total_pages < 7 %}
<div class="page-body col-md-12">
<ul class="pagination pagination-centered">
{% if paginator.total_pages >= 10 %}
{% if paginator.previous_page %}
<li>
««
</li>
{% else %}
<li class="disabled">
<a>««</a>
</li>
{% endif %}
{% endif %}
{% if paginator.previous_page %}
{% if paginator.previous_page == 1 %}
<li>
«
</li>
{% else %}
<li>
«
</li>
{% endif %}
{% else %}
<li class="disabled">
<a>«</a>
</li>
{% endif %}
{% if paginator.page == 1 %}
<li class="active">
<a>1</a>
</li>
{% else %}
<li>
1
</li>
{% endif %}
{% for count in (2..paginator.total_pages) %}
{% if count == paginator.page %}
<li class="active">
<a>{{count}}</a>
</li>
{% else %}
<li>
{{count}}
</li>
{% endif %}
{% endfor %}
{% if paginator.next_page %}
<li>
»
</li>
{% else %}
<li class="disabled">
<a>»</a>
</li>
{% endif %}
{% if paginator.total_pages >= 10 %}
{% if paginator.next_page %}
<li>
»»
</li>
{% else %}
<li class="disabled">
<a>»»</a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
{% else %}
{% assign page_start = paginator.page | minus: 2 %}
{% assign page_end = paginator.page | plus: 2 %}
{% if page_end > paginator.total_pages %}
{% assign page_end = paginator.total_pages %}
{% assign page_start = paginator.page | minus: 4 %}
{% endif %}
{% if page_start < 2 %}
{% assign page_end = paginator.page | plus: 3 %}
{% assign page_start = paginator.page | minus: 1 %}
{% endif %}
{% if page_start == 0 %}
{% assign page_end = paginator.page | plus: 4 %}
{% assign page_start = paginator.page %}
{% endif %}
<div class="page-body col-md-12">
<ul class="pagination pagination-centered">
{% if paginator.total_pages > 5 %}
{% if paginator.previous_page %}
<li>
««
</li>
{% else %}
<li class="disabled">
<a>««</a>
</li>
{% endif %}
{% endif %}
{% if paginator.previous_page %}
{% if paginator.previous_page == 1 %}
<li>
«
</li>
{% else %}
<li>
«
</li>
{% endif %}
{% else %}
<li class="disabled">
«
</li>
{% endif %}
{% if page_start == 1 %}
{% assign page_end = paginator.page | plus: 4 %}
{% assign page_start = 2 %}
{% if paginator.page == 1 %}
<li class="active">
1
</li>
{% else %}
<li>
1
</li>
{% endif %}
{% endif %}
{% for count in (page_start..page_end) %}
{% if count == paginator.page %}
<li class="active">
{{count}}
</li>
{% else %}
<li>
{{count}}
</li>
{% endif %}
{% endfor %}
{% if paginator.next_page %}
<li>
»
</li>
{% else %}
<li class="disabled">
»
</li>
{% endif %}
{% if paginator.total_pages > 5 %}
{% if paginator.next_page %}
<li>
»»
</li>
{% else %}
<li class="disabled">
<a>»»</a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
Try this instead
{% if paginator.total_pages < 7 %}
{% if paginator.page == 1 %}
<span class="current bold">1</span>
{% else %}
1
{% endif %}
{% for count in (2..paginator.total_pages) %}
{% if count == paginator.page %}
<span class="current bold">{{ count }}</span>
{% else %}
{{ count }}
{% endif %}
{% endfor %}
{% else %}
{% assign page_start = paginator.page | minus: 2 %}
{% assign page_end = paginator.page | plus: 2 %}
{% if page_end > paginator.total_pages %}
{% assign page_end = paginator.total_pages %}
{% assign page_start = paginator.page | minus: 4 %}
{% endif %}
{% if page_start < 2 %}
{% assign page_end = paginator.page | plus: 3 %}
{% assign page_start = paginator.page | minus: 1 %}
{% endif %}
{% if page_start == 0 %}
{% assign page_end = paginator.page | plus: 4 %}
{% assign page_start = paginator.page %}
{% endif %}
{% if page_start == 1 %}
{% assign page_end = paginator.page | plus: 4 %}
{% assign page_start = 2 %}
{% if paginator.page == 1 %}
<span class="current bold">1</span>
{% else %}
1
{% endif %}
{% endif %}
{% for count in (page_start..page_end) %}
{% if count == paginator.page %}
<span class="current bold">{{ count }}</span>
{% else %}
{{ count }}
{% endif %}
{% endfor %}
{% endif %}

Resources