Your issue is that as soon as you call .find()
, your ElementCriteriaModel returns an array of elements. In other words, the ElementCriteriaModel is gone, and it's no longer possible to add parameters (i.e. id()
) or call other methods that you'd otherwise be able to call on that object (i.e. .first()
).
On the other hand – like you say in your description; if you don't use .find()
(keeping the ElementCriteriaModel around) you'll hit the database every time you use the .id()
method.
A workaround could be to loop over the categories and add them to an indexed array (using the category IDs as keys) and then pull your individual categories from that index instead of the original array.
Important: One gotcha to note is that the |merge
filter will actually renumber numeric keys, so you have to add an arbitrary character to the ID to retain the actual ID values (the example below uses an underscore prefix):
{# Get the categories #}
{% set categories = craft.categories.group('products').limit(null).find() %}
{# Create category index #}
{% set categoryIndex = {} %}
{% for category in categories %}
{% set categoryIndex = categoryIndex|merge({('_'~category.id): category}) %}
{% endfor %}
{# Do your thing #}
<h1><a href="{{ categoryIndex['_102'].url }}">{{ categoryIndex['_102'].title|raw }}</a></h1>
<h2>{{ categoryIndex['_126'].title|raw }}</h2>
Note that when you want to pull a category from the index, categoryIndex['_102']
and categoryIndex._102
will both work – I just find that the former looks a bit cleaner, with the underscore in front of it (obviously you can use any character, it doesn't have to be an underscore).
Edit
As pointed out by @carlcs in the comments below, another approach to creating the index is to use the group
filter. With this approach you'll avoid the underscore hack and the for
loop. The gotcha here is that this filter will actually create an array of arrays, meaning you'll have to use categoryIndex[102][0]
(note the [0]
at the end) to access a specific category (you could also use categoryIndex[102]|first
, of course):
{% set categories = craft.categories.group('products').limit(null).find() %}
{% set categoryIndex = categories|group('id') %}
<h1><a href="{{ categoryIndex[102][0].url }}">{{ categoryIndex[102][0].title|raw }}</a></h1>
<h2>{{ categoryIndex[126][0].title|raw }}</h2>
.find()
(which you have), then you're no longer dealing with an Element Criteria Model. Thefind
parses the ECM into an array of elements (in this case, an array of Category Models).for
loop as soon as you have a match. If that doesn't float your boat, I added an answer with a different approach below – it requires looping through all the categories, but only once :)