1

I have a category tree like this and I have an entry that has four categories A, A1, B, C2:

  • A
    • A1
    • A2
  • B
    • B1
    • B2
  • C
    • C1
    • C2

I want a query that ignores A and returns only A1, B, C2, because I don't want to display the parent category of a sub-category that's also displayed.

{% for category in entry.businessTypes.all() %}{{ category.title }}{% endfor %} returns all: A, A1, B, C2

{% for category in entry.businessTypes.leaves().all() %}{{ category.title }}{% endfor %} returns A2, C2 but not B.

{% for category in entry.businessTypes.hasDescendants().all() %}{{ category.title }}{% endfor %} returns A, B but not A1, C2.

Any suggestions are appreciated.

2
  • hey travis, did you find a solution for this problem? Feb 9 at 13:40
  • @MatthiasRedl-Mann I've just posted an answer with two working solutions, even though they do the filtering in memory instead of in the database query itself.
    – MoritzLost
    Sep 13 at 14:49
1

I could not test it, but you could try:

If you always have a Parent and 2 levels

entry.businessTypes.level('>= 2').all()

If you want to only get the lowest, you could try to assign it with a condition:

{% if categories = entry.businessTypes.children %} 
    {% set categories = entry.businessTypes.level('>= 2').all() %}
{% else categories = entry.businessTypes.children %} 
    {% set categories = entry.businessTypes.all() %}
{% endif %}

{% for category in categories %} ... {% endfor %}
1

A bit late, but I've just come across this very problem and found a solution that works well enough. Thought I'd post it here in case anyone is trying to do something similar.


As far as I can tell, doing this in the database query itself (without too many complicated custom selects / conditions) is not feasible. Category queries do have an option to only return leaf nodes (categories without a descendant), but that doesn't match the problem. After all, we do want to display categories that don't have leaf nodes if none of those leaves are selected.

So the only solution I could find is to fetch all selected categories and then filter them. We want to filter out all categories with descendants that are selected on this entry. We can take advantage of the fact that Craft automatically selects all ancestors of a term when a lower-level term is selected. Because of this, we know that for every category that we don't want to show, one other category in the list will have this category as it's parent. So we can filter the list by checking for this condition:

{% set allSelectedCategories = entry.businessTypes.all() %}
{% set leafNodesOnly = allSelectedCategories|filter(category =>
    (allSelectedCategories|filter(
        c => c.parent is not empty and c.parent.id == category.id
    ) is empty)
) %}

The problem with that is that it has quadratic complexity since for every node, we're checking every node (the fact that Twig doesn't have something like Array.prototype.some doesn't help). If your entries have many categories, this may slow down the template considerably.


We can also take advantage of the fact that category queries return their results in hierarchical order by default. So instead of checking if ANY category in the array is a child of the current one, we can just check the next category in the list. If it isn't a child of the current category, the current category in the loop is a leaf node.

{% set allSelectedCategories = entry.businessTypes.all() %}
{% set leafNodesOnly = allSelectedCategories|filter((category, index) =>
    allSelectedCategories[index + 1] is not defined or
    allSelectedCategories[index + 1].parent is empty or
    allSelectedCategories[index + 1].parent.id != category.id
) %}

However, this approach does not work if you've changed the sort order to anything other than the default.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.