4

I am trying to filter using categories. I have to filters type and size, in the url.

/products.html?size=medium,large&type=polo-shirt,jeans

In my template, I am doing the following

{% set sizes = craft.app.request.getQueryParam('size') %}
{% set types = craft.app.request.getQueryParam('type') %}

{% set relationParam = ['and'] %}

{# type #}
{% if types %}
  {% for i in types|split(",") %}
    {% set type = craft.categories.group('types').slug(i) %}
    {% set relationParam = relationParam|merge([{ targetElement: type }]) %}
  {% endfor %}
{% endif %}

{# sizes #}
{% if sizes %}
  {% for i in sizes|split(",") %}
    {% set size = craft.categories.group('sizes').slug(i) %}
    {% set relationParam = relationParam|merge([{ targetElement: size }]) %}
  {% endfor %}
{% endif %}

{# Avoid passing an array with `['and']` only #}
{% if relationParam|length < 2 %}
  {% set relationParam = '' %}
{% endif %}

{% set entries = craft.entries({
    section: 'products',
    relatedTo:  relationParam
}) %}

<div class="listings-container">
  {% if entries|length %}
    {% for entry in entries %}
        {% include "partials/_listing.html" %}
    {% endfor %}
  {% endif %}
</div>

The problem I'm having is as follows:

  1. I click on “medium” in sizes, it shows all products that have the category “medium”
  2. I then, click on “large” in sizes, it shows one result because it’s searching for products that have all 2 categories (“medium”, “large”)
  3. I then click on "polo-shirt" in types, this now shows all products that have all 3 categories (“medium”, “large", "polo-shirt")
  4. I then click on “jeans” in sizes, this now shows no results as its trying to find products that have all 4 categories (“medium”, “large", "polo-shirt", "jeans")

What I am trying to achieve is find all the products that are “medium” or “large" sizes and of "polo-shirt" or “jeans” types

  • 1
    Would it be an option to do it in PHP? Because it's a mess in Twig – Robin Schambach Jul 20 '18 at 16:33
  • @RobinSchambach nope, it needs to be Twig I'm afraid – Hassan Mahmood Jul 20 '18 at 16:38
6

The issue with your current code is that you're looping over each type/size category, creating a parameter in your relatedTo query for each one. This means that you're currently creating a query that looks like this:

relatedTo([
    'and',
    { targetElement: 1 },
    { targetElement: 2 },
    { targetElement: 3 },
    { targetElement: 4 }
])

...which, combined with the AND, means that Craft will only return entries that have all of these relations.

What you need to do, is to collect the IDs for each category group in an array, and add those arrays to the relatedTo query – i.e. create a query that looks like this:

relatedTo([
    'and',
    { targetElement: [1, 2] },
    { targetElement: [3, 4] }
])

The above query would solve your issue, because whenever Craft isn't given an and or or, it assumes or. This means that the above query is exactly the same as the below:

relatedTo([
    'and',
    { targetElement: ['or', 1, 2] },
    { targetElement: ['or', 3, 4] }
])

Essentially, the above query is saying: give me all entries that have a relation matching either the target element 1 OR 2, AND a relation matching either the target element 3 or 4.

Here's how the Twig to create a query like the above could look:

{# Create the base query #}
{% set query = craft.entries({ section: 'products' }) %}

{% set relatedTo = [] %}

{% set types = craft.app.request.getQueryParam('type')|split(',')|filter %}
{% if types %}
    {% set relatedTo = relatedTo|merge([{
        targetElement: craft.categories({ group: 'types', slug: types }).ids()
    }]) %}
{% endif %}

{% set sizes = craft.app.request.getQueryParam('size')|split(',')|filter %}
{% if sizes %}
    {% set relatedTo = relatedTo|merge([{
        targetElement: craft.categories({ group: 'sizes', slug: sizes }).ids()
    }]) %}
{% endif %}

{% if relatedTo|length > 1 %}
    {% set relatedTo = ['and']|merge(relatedTo) %}
{% endif %}

{% set entries = query.relatedTo(relatedTo ? relatedTo : null).all() %}

The above code should work for all the below use cases:

  • /products (returns all product entries)
  • /products?type=polo-shirt (returns all polo shirts)
  • /products?type=polo-shirt&size=small (returns all small polo shirts)
  • /products?type=polo-shirt,jeans&size=small (returns all small polo shirts and jeans)
  • /products?type=polo-shirt,jeans&size=small,medium (returns all small and medium polo shirts and jeans)

Note that the usage of ids() over all() in a few places (such as in the craft.categories queries to get the type and size categories) – this is a performance tweak (it's more effective to just pull the IDs than the entire model).

Also note that if you want to use pagination, just amend the final line to

{% paginate query.relatedTo(relatedTo ? relatedTo : null).limit(10) as pageInfo, pageEntries %}

Note: I originally typed out a completely different answer to this question, but after some helpful discussion with @Robin Schambach on Slack, we arrived on the above, IMO much better solution. Cheers Robin!

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.