2

I'm displaying lists of members grouped by their surname initial which the user navigates using a list of letters from A to Z. This part was straightforward enough and the following code works to return all members with a particular initial.

{% set searchString = 'surname:' ~ searchLetter ~ '*' %}
{% set members = craft.entries.section('members').search(searchString).order('surname', 'firstName') %}

Where I'm struggling is listing only the letters for which there are members to use for navigation, or to be able to list them all but disable those that don't have members.

Currently I just loop over the characters in the alphabet and output all of them.

{% set characters = 'a'..'z' %}

If someone clicks on a letter that has no members they get a blank list, but this isn't ideal as it would be better if there wasn't a link to this page.

I know with SQL I could do something like a SELECT DISTINCT with grouping to return just a list of initials in use, but is this possible with Twig or would I need to look at writing a plugin?

7

[untested]

Something like this should do the trick:

{% for letter, members in craft.entries.section('members').order('surname', 'firstname') | group('surname|slice(0,1)') %}
    <h1>{{ letter }}</h1>
    <ul>
    {% for member in members %}
        <li>{{ member.surname }}, {{ member, firstname }}</li>
    {% endfor %}
    </ul>
{% endfor %}
  • Thanks, that does work although I had to add limit(null) in order to get all results, Craft must have a natural limit to entries returned. However, under the hood this is returning all of the members and their data which has a very noticeable effect on the page load time. I'm looking for a method which just returns a list/array of the letters from the db. I know caching will help but seems a heavy handed approach to pull out all of the data for this purpose. – Gogster Aug 12 '15 at 12:32
  • 1
    Default limit is 100, so limit(null) will do the trick. What you want, can not be done (I think) through Twig. You can get an array of ids by adding .ids() to you craft.entries query, but then you loose the ability to group by first letter of surname. I think creating a plugin with a little bit of logic will do the trick. All you need within your plugin is a variable and a service. Service gets the info you want with one DB query and returns it to Twig. – Paul Aug 12 '15 at 12:40
  • That's what I wanted to know, thanks. Will have to get my hands dirty with a plugin then! – Gogster Aug 12 '15 at 14:35
  • Let me know if you need any advice! – Paul Aug 12 '15 at 14:36
  • The plugin worked a treat, much more efficient. Thanks again for the pointer. – Gogster Aug 12 '15 at 16:08
4

The answer is that it is possible in Twig but not very efficient so I ended up writing a very simple plugin with just two files (aside from the main plugin file).

One in variables subfolder MyPluginVariable.php:

<?php
namespace Craft;
class MyPluginVariable
{
public function getAToZ()
{
    return craft()->myPlugin_members->getAToZ();
}
}

And the other in services subfolder MyPlugin_MembersService.php:

<?php
namespace Craft;

class MyPlugin_MembersService extends BaseApplicationComponent
{
public function getAToZ()
{   
    $query = craft()->db->createCommand()
        ->selectDistinct('SUBSTRING(field_surname,1,1) AS letter')
        ->from('content')
        ->order('letter asc')
        ->queryAll();
    return($query);
}
}

Then I could access the data in my template:

{% for letter in craft.myPlugin.getAToZ() %}
{{ letter['letter'] }}

To loop over the letters. There is a very noticeable improvement in performance doing it this way.

  • Looking to implement something similar myself, I have the above working. But ideally I would like to output all a-z and then add a class of disabled to any letters that don't have entries...any ideas how to achieve this? – Terry Upton Jul 18 '17 at 16:16
  • I'd suggest something like using a loop for all characters and then checking within that if it exists in the db e.g. (untested) {% set letters = craft.myPlugin.getAToZ()%}{% for letter in 'a'..'z' %}{% if letter in letters%} enable {% else %} disable {endif %}{% endfor %} – Gogster Jul 19 '17 at 19:46
  • This is probably more accurate... {% set letters = [] %}{% for letter in craft.myPlugin.getAToZ() %}{% set letters = letters|merge(letter['letter']) %}{% endfor %} {% for letter in 'a'..'z' %}{% if letter in letters %} enable {% else %} disable {endif %}{% endfor %} – Gogster Jul 19 '17 at 19:57
  • Thanks for the reply. However, I am seeing the error; The merge filter only works with arrays or "Traversable", got "NULL" as second argument. – Terry Upton Jul 20 '17 at 14:31
  • Change {% set letters = letters|merge(letter['letter']) %} to {% set letters = letters|merge([letter['letter']]) %} - also, just be aware that you may need to use {% for letter in 'A'..'Z' %} in order to match case – Gogster Jul 20 '17 at 15:04

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.