You’ve built a beautiful set of product filters that, when selected, narrow the results of available products in an online store. You’ve also figured out how to append the filter values to the URL so that links can be saved and shared. But at the final hour, the “marketing department” swoops in and presents a “non-negotiable”:
Filter values must appear in the URL in a specific order, for example:
garmentsforducks.com/products?cut=slim&colour=green&size=m&style=casual
Rather than ask why (you learned your lesson last time you challenged an unusual request of theirs), you decide to get right to it so that no ducks get left out in the cold.
The filters are saved as entries in a structure section called Filters
. The filter values are stored in an associative array (a hash, as it’s called in Twig) called filterValues
. You need to ensure that they appear in the exact order as the entries in the Filters
section.
Here are some sample inputs and outputs.
{% set filterValues = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
{# Your solution code #}
{{ filterValues }}
should output
cut=slim&colour=green&size=m&style=elegant
{% set filterValues = { size: 's', colour: 'yellow' } %}
{# Your solution code #}
{{ filterValues|url_encode }}
should output
colour=yellow&size=s
{% set filterValues = { cut: 'regular' } %}
{# Your solution code #}
{{ filterValues|url_encode }}
should output
cut=regular
This GitHub repo contains a Craft CMS site that you can spin up with a single command, either locally or in the browser using a GitHub codespace (see the readme file in the repo). The index.twig template contains sample inputs and outputs that you can use to test your solution as you work on it.
Your solution should consist of the Twig logic that first fetches the filters entries and then manipulates the filtersValues
variable to produced the expected output. It should work with any input using the format shown. No plugins or modules may be used.
This challenge can be solved using Twig filters or some lesser-known Collection methods in a rather elegant way (without requiring any for
loops). The “key” lies in how you query the filter entries.
Before we write any code, it is crucial to understand what it is we need to do, so below are the steps that we will be implementing:
The first thing we’ll need to do is to fetch the filters, which requires executing an entry query. How we do this, however, depends on the approach we’ll take, of which there are a few possibilities.
Looping over the filters is the simplest and clunkiest way of solving this challenge, but since it’s the easiest to grasp, let’s cover it first.
Since we’re really only concerned with the filter slugs, we can add a .select()
parameter to the entry query to reduce what we get back, followed by calling .column()
to return the result as an array of values.
{% set order = craft.entries.section('filters')
.select('slug')
.column()
%}
This results in order
having the value:
[ 'cut', 'colour', 'size', 'style' ]
Next we’ll create a new associative array to hold the ordered values and loop over the filters. We’ll only add values that exist in the filters array.
{% set orderedFilters = {} %}
{% for key in order %}
{% if filterValues[key] is defined %}
{% set orderedFilters = orderedFilters|merge({ (key): filterValues[key] }) %}
{% endif %}
{% endfor %}
Finally, we can URL encode the ordered filters to give us the desired result.
{% set orderedFilters = values|url_encode %}
We can achieve the same result in a more elegant solution using the map
filter. To set things up, we begin fetching the filters, but this time we’ll add an indexBy
parameter to the entry query to assign the slugs to both the keys and values, followed by calling .column()
to return the result as an associated array of key-value pairs.
{% set order = craft.entries.section('filters')
.indexBy('slug')
.column()
%}
This results in order
having the value:
{ cut: 'cut', colour: 'colour', size: 'size', style: 'style' }
We’ll use the map
filter to set the filter values to the values of the order
array with the appropriate keys. Since some of these values may be empty, we’ll ensure that they fall back to a value of null
, which will then allow us to remove any empty values using the filter
filter (you read that right!), before URL encoding the result.
{% set orderedFilters = order|map(value => filterValues[value] ?? null)
|filter
|url_encode
%}
Similar solutions: Fred Carlsen, Chris Sargeant.
Collections provide some lesser-known methods that we can use to manipulate the filters to give the desired result. We’ll again begin by fetching the filters, but we’ll go all-in on Collection methods and use the pluck
method to extract only the slugs, followed by the flip
method to assign the slugs to the keys.
{% set order = craft.entries.section('filters')
.collect()
.pluck('slug')
.flip()
%}
This results in a collection of slugs.
We’ll first flip the collection, resulting in the slugs being assigned to the keys. Next we intersect the slugs with the keys of the filters, which will ensure that only matching filters are included. Then we’ll merge the values, which will be assigned according to their keys, maintaining the order that we want.
{% set orderedFilters = order.intersectByKeys(filterValues)
.merge(filterValues)
%}
Finally, we need to call .all()
to convert the Collection to an array before URL encoding it.
{% set orderedFilters = order.intersectByKeys(filterValues)
.merge(filterValues)
.all()
|url_encode
%}
Similar solutions: Andrew Welch, Robin Gauthier, Matteo Fogli, Ferre Lambert.
Solution submitted by Andrew Welch on 11 September 2023.
{#
# Challenge #11 – Get your Ducks in a Row
# https://www.craftcodingchallenge.com/challenge-13-get-your-ducks-in-a-row/
# @author Andrew Welch, nystudio107
#}
{% extends '_layout.twig' %}
{#
# Macro that returns a URL-encoded string of the `values` Collection in the order specified
# in the `filters` Structure in Craft CMS.
#
# Take advanted of the fact that when you merge an associative array (aka a Twig "hash"),
# it will preserve the key order in the original array
#
# @param values Collection Key/value pairs for the filters
# @return string A URL-encoded string of the key/value pairs in the order specified by `filters`
#}
{% macro filterOrder(values) %}
{% set filters = craft.entries.section('filters').collect().pluck('slug') %}
{% set filterValues = filters.flip().intersectByKeys(values).merge(values) %}
{{ filterValues.all()|url_encode }}
{% endmacro %}
{% block body %}
{% set filterValues = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
{# Your solution code #}
{% set filterValues = _self.filterOrder(filterValues) | spaceless %}
<div class="text-center font-mono leading-loose">
{{ filterValues|raw }}
<div class="font-sans text-gray-500">should equal</div>
cut=slim&colour=green&size=m&style=elegant
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { size: 's', colour: 'yellow' } %}
{# Your solution code #}
{% set filterValues = _self.filterOrder(filterValues) | spaceless %}
<div class="text-center font-mono leading-loose">
{{ filterValues|raw }}
<div class="font-sans text-gray-500">should equal</div>
colour=yellow&size=s
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { cut: 'regular' } %}
{# Your solution code #}
{% set filterValues = _self.filterOrder(filterValues) | spaceless %}
<div class="text-center font-mono leading-loose">
{{ filterValues|raw }}
<div class="font-sans text-gray-500">should equal</div>
cut=regular
</div>
{% endblock %}
Solution submitted by Fred Carlsen on 12 September 2023.
{% extends '_layout.twig' %}
{% macro sortFilters(filters = {}) %}
{# Get filter entries and map slug and index so we can use them in later steps #}
{% set filterEntries = craft.entries.section('filters').all()|map((val, index) => ({ slug: val.slug, index: index })) %}
{#
We use filterEntries as the source to loop over, as that will let us easily check if the slug of a filter entry exists in the passed filters.
If slug is not set in the passed filters object, we return null. Finally we use `filter` to filter non-existing ones.
#}
{% set mappedFilterEntries = filterEntries|map(val => attribute(filters, val.slug) ?? null ? { value: attribute(filters, val.slug) }|merge(val) : null )|filter %}
{#
We finally order by the index, and then use the `pluck` method, which takes two arguments: `value` and optinally `key`.
The method will use the value of the property passed in `key` to set the key of the retured array.
#}
{{ collect(mappedFilterEntries).sortBy('order').pluck('value', 'slug').toArray()|url_encode }}
{% endmacro %}
{% block body %}
{% set filterValues = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
{# Your solution code #}
<div class="text-center font-mono leading-loose">
{{ _self.sortFilters(filterValues) }}
<div class="font-sans text-gray-500">should equal</div>
cut=slim&colour=green&size=m&style=elegant
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { size: 's', colour: 'yellow' } %}
{# Your solution code #}
<div class="text-center font-mono leading-loose">
{{ _self.sortFilters(filterValues) }}
<div class="font-sans text-gray-500">should equal</div>
colour=yellow&size=s
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { cut: 'regular' } %}
{# Your solution code #}
<div class="text-center font-mono leading-loose">
{{ _self.sortFilters(filterValues) }}
<div class="font-sans text-gray-500">should equal</div>
cut=regular
</div>
{% endblock %}
Solution submitted by Robin Gauthier on 13 September 2023.
{% extends '_layout.twig' %}
{% block body %}
{# Your solution code #}
{% set order = craft.entries().section('filters').collect().pluck('slug').flip() %}
{% set filterValues = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
{# Your solution code #}
{% set filterValues = order.intersectByKeys(filterValues)|merge(filterValues) %}
<div class="text-center font-mono leading-loose">
{{ filterValues|url_encode }}
<div class="font-sans text-gray-500">should equal</div>
cut=slim&colour=green&size=m&style=elegant
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { size: 's', colour: 'yellow' } %}
{# Your solution code #}
{% set filterValues = order.intersectByKeys(filterValues)|merge(filterValues) %}
<div class="text-center font-mono leading-loose">
{{ filterValues|url_encode }}
<div class="font-sans text-gray-500">should equal</div>
colour=yellow&size=s
</div>
<hr class="w-full border border-gray-300">
{% set filterValues = { cut: 'regular' } %}
{# Your solution code #}
{% set filterValues = order.intersectByKeys(filterValues)|merge(filterValues) %}
<div class="text-center font-mono leading-loose">
{{ filterValues|url_encode }}
<div class="font-sans text-gray-500">should equal</div>
cut=regular
</div>
{% endblock %}
Solution submitted by Chris Sargeant on 15 September 2023.
{% macro sort_parameters(filters) %}
{{ craft.entries().section('filters').select('slug').column()
|filter(k => k in filters|keys)|map(k => {(k):filters[k]}|url_encode)
|merge(filters|keys|map(k => {(k):filters[k]}|url_encode))
|unique
|join('&')
}}
{% endmacro %}
{% set filters = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
<p>
{{ filters|url_encode }}<br>
<strong>{{ _self.sort_parameters(filters) }}</strong>
</p>
{% set filters = { size: 's', colour: 'yellow' } %}
<p>
{{ filters|url_encode }}<br>
<strong>{{ _self.sort_parameters(filters) }}</strong>
</p>
{% set filters = { cut: 'regular' } %}
<p>
{{ filters|url_encode }}<br>
<strong>{{ _self.sort_parameters(filters) }}</strong>
</p>
{% set filters = { foo: 'bar', hello: 'world' } %}
<p>
{{ filters|url_encode }}<br>
<strong>{{ _self.sort_parameters(filters) }}</strong>
</p>
{% set filters = { foo: 'bar', hello: 'world', style: 'bootcut', colour: 'brown' } %}
<p>
{{ filters|url_encode }}<br>
<strong>{{ _self.sort_parameters(filters) }}</strong>
</p>
Solution submitted by Matteo Fogli on 19 September 2023.
{% extends '_layout.twig' %}
{% block body %}
{% set filterValues = { style: 'elegant', colour: 'green', size: 'm', cut: 'slim' } %}
{#
Query entries, filtering entries that are not part of the current filter
Fetch as collection for further processing
Extract `slug` column
For each filter (slug) return an object with the filter slug as key, and the current filter filterValue
Flatten the nested collection and return as array
#}
{% set filterValues = craft.entries()
.section('Filters')
.slug(filterValues|keys)
.collect()
.pluck('slug')
.flatMap(k => { (k): filterValues[k] })
.toArray()
%}
<div class="text-center font-mono leading-loose">
{{ filterValues|url_encode }}
<div class="font-sans text-gray-500">should equal</div>
cut=slim&colour=green&size=m&style=elegant
</div>
{% endblock %}
Solution submitted by Ferre Lambert on 12 October 2023.
{% set sortValues = craft.entries.section('filters').select(["slug"]).column() %}
{% set filterValues = collect(sortValues).flip().merge(filterValues).intersectByKeys(filterValues).all() %}