Challenge #13 – Get your Ducks in a Row

11 September 2023 Solved Twig Intermediate

You’ve built a beau­ti­ful set of product fil­ters that, when selec­ted, nar­row the res­ults of avail­able products in an online store. You’ve also figured out how to append the fil­ter val­ues to the URL so that links can be saved and shared. But at the final hour, the mar­ket­ing depart­ment” swoops in and presents a non-nego­ti­able”:

Fil­ter val­ues must appear in the URL in a spe­cif­ic order, for example:

garmentsforducks.com/products?cut=slim&colour=green&size=m&style=casual

Rather than ask why (you learned your les­son last time you chal­lenged an unusu­al request of theirs), you decide to get right to it so that no ducks get left out in the cold.

The fil­ters are saved as entries in a struc­ture sec­tion called Filters. The fil­ter val­ues are stored in an asso­ci­at­ive 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.

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 Git­Hub repo con­tains a Craft CMS site that you can spin up with a single com­mand, either loc­ally or in the browser using a Git­Hub codespace (see the readme file in the repo). The index.twig tem­plate con­tains sample inputs and out­puts that you can use to test your solu­tion as you work on it.

Sample inputs and outputs

Rules

Your solu­tion should con­sist of the Twig logic that first fetches the fil­ters entries and then manip­u­lates the filtersValues vari­able to pro­duced the expec­ted out­put. It should work with any input using the format shown. No plu­gins or mod­ules may be used.

Tips

This chal­lenge can be solved using Twig fil­ters or some less­er-known Col­lec­tion meth­ods in a rather eleg­ant way (without requir­ing any for loops). The key” lies in how you query the fil­ter entries.

Acknow­ledge­ments

  • Writ­ten by Ben Croker 
  • Dock­er setup by Andrew Welch (Spin Up Craft)

Solution

Before we write any code, it is cru­cial to under­stand what it is we need to do, so below are the steps that we will be implementing:

  1. Fetch the slugs of the fil­ter entries.
  2. Cre­ate an asso­ci­at­ive array (a hash).
  3. For each fil­ter, set the key to the slug and the value to the filter’s value.
  4. Remove any remain­ing empty fil­ter val­ues (fil­ters that were not specified).

The first thing we’ll need to do is to fetch the fil­ters, which requires execut­ing an entry query. How we do this, how­ever, depends on the approach we’ll take, of which there are a few possibilities.

For Loop

Loop­ing over the fil­ters is the simplest and clunki­est way of solv­ing this chal­lenge, but since it’s the easi­est to grasp, let’s cov­er it first.

Since we’re really only con­cerned with the fil­ter slugs, we can add a .select() para­met­er to the entry query to reduce what we get back, fol­lowed by call­ing .column() to return the res­ult as an array of values.

{% set order = craft.entries.section('filters')
    .select('slug')
    .column()
%}

This res­ults in order hav­ing the value:

[ 'cut',  'colour',  'size',  'style' ]

Next we’ll cre­ate a new asso­ci­at­ive array to hold the ordered val­ues and loop over the fil­ters. We’ll only add val­ues that exist in the fil­ters 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 fil­ters to give us the desired result.

{% set orderedFilters = values|url_encode %}

Twig Fil­ters

We can achieve the same res­ult in a more eleg­ant solu­tion using the map fil­ter. To set things up, we begin fetch­ing the fil­ters, but this time we’ll add an indexBy para­met­er to the entry query to assign the slugs to both the keys and val­ues, fol­lowed by call­ing .column() to return the res­ult as an asso­ci­ated array of key-value pairs.

{% set order = craft.entries.section('filters')
    .indexBy('slug')
    .column()
%}

This res­ults in order hav­ing the value:

{ cut: 'cut',  colour: 'colour',  size: 'size',  style: 'style' }

We’ll use the map fil­ter to set the fil­ter val­ues to the val­ues of the order array with the appro­pri­ate keys. Since some of these val­ues may be empty, we’ll ensure that they fall back to a value of null, which will then allow us to remove any empty val­ues using the filter fil­ter (you read that right!), before URL encod­ing the result.

{% set orderedFilters = order|map(value => filterValues[value] ?? null)
    |filter
    |url_encode 
%}

Sim­il­ar solu­tions: Fred Carlsen, Chris Sar­geant.

Col­lec­tions

Col­lec­tions provide some less­er-known meth­ods that we can use to manip­u­late the fil­ters to give the desired res­ult. We’ll again begin by fetch­ing the fil­ters, but we’ll go all-in on Col­lec­tion meth­ods and use the pluck meth­od to extract only the slugs, fol­lowed by the flip meth­od to assign the slugs to the keys.

{% set order = craft.entries.section('filters')
    .collect()
    .pluck('slug')
    .flip()
%}

This res­ults in a col­lec­tion of slugs.

We’ll first flip the col­lec­tion, res­ult­ing in the slugs being assigned to the keys. Next we inter­sect the slugs with the keys of the fil­ters, which will ensure that only match­ing fil­ters are included. Then we’ll merge the val­ues, which will be assigned accord­ing to their keys, main­tain­ing the order that we want.

{% set orderedFilters = order.intersectByKeys(filterValues)
    .merge(filterValues) 
%}

Finally, we need to call .all() to con­vert the Col­lec­tion to an array before URL encod­ing it.

{% set orderedFilters = order.intersectByKeys(filterValues)
    .merge(filterValues) 
    .all()
    |url_encode 
%}

Sim­il­ar solu­tions: Andrew Welch, Robin Gau­th­i­er, Mat­teo Fogli, Ferre Lam­bert.

Submitted Solutions

  • Andrew Welch
  • Fred Carlsen
  • Robin Gauthier
  • Chris Sargeant
  • Matteo Fogli
  • Ferre Lambert