5

I am just starting out with Craft CMS.

What I see right now is a lot of static content in the html templates. Even in the demo.

{% block content %}
<h1>{{ entry.title }}</h1>

{{ entry.body }}

<h2>Recent News</h2>
<ul>
    {% for entry in craft.entries.section('news').limit(5).find() %}
        <li><a href="{{ entry.url }}">{{ entry.title }}</a></li>
    {% endfor %}
</ul>
{% endblock %}

You see, that is very static. Is there a way to make it all more flexible and dynamic?

I want my site to be totally flexible so that users can change the outcome completely with dragging and dropping sections in the admin area.

I only want to define HTML for a field / section. Then, I would simply need to loop all fields through and load the html template for each section that is being used.

This would allow massive reusability of these 'content boxes' everywhere. Basically, it'll become drag and drop website building to a large extend.

Do you have some advice on how to do this best?


Basically this guy is talking about it: https://mijingo.com/products/screencasts/reusable-twig-templates-craft/

1 Answer 1

11

I'm not sure I understand your question 100%, but I'm assuming you're talking about a thoroughly modular approach to templating, and not about letting your end users/content editors actually edit the content model (i.e. create/edit Sections, fields, field layouts etc) – because the latter is not a good approach, in general.

Either way, this is a very open question, so this answer is kind of broad (or in any case, it just outlines one way to do it, focused on entries and fields).

Note that I haven't seen the Mijingo videos, but I think the answer to your use case is probably to go with a partial-heavy approach. Basically, a "partial" is an incomplete/modular template that you can include in another template.

First off, you might want to create a "catch-all" template (let's just assume your catch-all template is called templates/index.html). Be sure to add the path to that template (which would just be index) to the Entry Template setting for all relevant Sections, to make sure the same, catch-all template is always loaded for all entry URLs.

Then, assuming your site has three fields – heading, image and body – you'd create a partial template for each of these fields. It's a good idea to store your partials in a folder prefixed with an underscore, to make sure Craft doesn't include them whenever it maps URL routes to template paths (these templates aren't standalone and should never be rendered on their own). So for the three fields in our example, you'd create these three templates:

/templates/_partials/heading.html
/templates/_partials/image.html
/templates/_partials/body.html

Here's how the heading.html partial could look:

{% if element.heading %} <h1 class="heading">{{ element.heading }}</h1> {% endif %}

Inside your catch-all template, you'd then need to {% include %} these partial templates, following some logic of your choice.

Here's an approach that basically loops through all fields for the current entry's field layout, attempting to include a partial template for each field:

{% extends '_layout' %}{# Assuming this template extends a basic layout template #}
{% block content %}

    {# Get all fields, indexed by field ID #}
    {% set allFields = craft.fields.getAllFields('id') %}

    {# Get the IDs for all fields in this entry's field layout #}
    {% set fieldIds = entry.getFieldLayout().getFieldIds() %}

    {# Loop through the entry fields' IDs; attempt to include template based on each field's handle #}
    {% for fieldId in fieldIds %}
        {% set field = allFields[fieldId] ?? null %}
        {% if field and entry[field.handle] %}
            {% include '_partials/'~field.handle ignore missing with { element: entry } %}
        {% endif %}
    {% endfor %}

{% endblock %}

A couple of things to note:

First, the above code builds the template path to each partial dynamically, by joining the root path ('_partials/') with each field's handle property (using the tilde operator ~). In other words, this solution hinges upon using the fields' handles for the partial templates' filenames. Note that its also possible to pass an array of template paths to the {% include %} tag, in which case Craft will attempt to load the next template in that array whenever it fails the load the former. Here's how that could look:

{% include ['_partials/matrix/'~field.handle, '_partials/fields/'~field.handle] ignore missing with { element: entry } %}`

With the above, Craft would first look for the template /templates/_partials/matrix/title.html, and if that file doesn't exist it'd look for /templates/_partials/fields/title.html (before failing silently due to the ignore missing directive).

Second, it might seem redundant to pull all fields from the FieldsService, but the alternative is to hit the database for every field in your entry's field layout, so this should actually be the most performant way to pull the current entry's actual FieldModels.

Third – the with statement for the {% include %} tag enables you to pass parameters to the included template. In this example, we're actually exposing the current entry to the included template as the more generic element variable. This is just future proofing in case we want to use the same partials for other element types, e.g. categories (in which case you'd use with { element: category }) – notice that in the example heading.html template above, element.heading is used over entry.heading.

If you want to avoid variable pollution, you might pass the only directive aswell, in which case only whatever parameters passed with with will be made available to the included statement, nothing else:

{% include '_partials/'~field.handle with { element: entry } only %}

Of course, you can include other stuff in the object passed to with, as well. For instance, you might want to render stuff differently if the current field is either first or last in the loop:

{% include '_partials/'~field.handle with { element: entry, isFirst: loop.first, isLast: loop.last } %}

{# In heading.html #}
<div class="field{{ isFirst ? ' is-first' }}{{ isLast ? ' is-last' }}">
    <h2 class="heading">{{ element.heading }}</h2>
</div>

Going further; talking about Matrix fields and alluding to the fact that putting Matrix field partials in a separate templates folder might be a good idea – if you're going fully modular it might make sense to create separate templates for each Matrix block type, as well. Tons of ways to skin this cat, but one way to do it is to add a sub folder for every Matrix field, containing partials for each of the field's block types. For example, if you've got a Matrix field called contentBlocks with the block types images and text you might structure it like this:

templates/_partials/contentBlocks.html (the partial for the Matrix field itself)
templates/_partials/contentBlocks/images.html (the partial for the images block type)
templates/_partials/contentBlocks/text.html (the partial for the text block type)

Then, inside the contentBlocks.html template, you could do something like this:

{% for block in element.contentBlocks %}
    {% include '_partials/contentBlocks/'~block.type ignore missing with { block: block } only %}
{% endfor %}

In fact, you might not even need the contentBlocks.html field template, and just do something like this in index.html:

{% if field.type == 'Matrix' %}
    {# Attempt to include the blocks for this Matrix field #}
    {% for block in entry.field %}
        {% include '_partials/'~field.handle~'/'~block.type ignore missing with { block: block } only %}
    {% endfor %}
{% else %}
    {# Non-Matrix fields #}
    {% include '_partials/'~field.handle ignore missing with { element: entry } %}
{% endif %}

One final thing to note – with the above solution, your entries' fields' partials will be included in the order corresponding to the entry type's field layout (i.e. the first field in the layout will be included first). This may or may not be what you want (the order that makes sense inside the CP may not always correspond to what the design/front end should look like) but if you need more control over your fields' order, you could do something like this inside index.html:

{% set fieldsToInclude = ['image', 'heading', 'body'] %}
{% for fieldHandle in fieldsToInclude %}
    {% if entry['fieldHandle'] %}
        {% include '_partials/'~fieldHandle ignore missing with { element: entry ) %}
    {% endif %}
{% endfor %}

The above will actually be slightly more performant than the first example, because it avoids the few database queries involved in pulling the fields dynamically.

Of course, for optimal flexibility you'd probably go heavy on the Matrix fields, too – obviously, any Matrix block partials would render in their original order inside the Matrix fields, so with Matrix blocks your content editors will have complete control over where each piece of content actually renders.

8
  • 7
    Epic answer. A+++. Would read again.
    – Brad Bell
    Commented Dec 19, 2016 at 0:32
  • Thank you! ` {# Loop through the entry fields' IDs; attempt to include template based on each field's handle #} {% for fieldId in fieldIds %} {% set field = allFields[fieldId] ?? null %} {% if field.type == 'Matrix' %} {# Attempt to include the blocks for this Matrix field #} {% for block in entry.field %} {% include '_partials/'~field.handle~'/'~block.handle ignore missing with { block: block } only %}`
    – Jon Riel
    Commented Dec 21, 2016 at 1:25
  • 1
    If field is a variable and not an actual field handle, you'll have to use something like {% for block in entry[field.handle] %}. Commented Dec 21, 2016 at 1:33
  • 2
    Sorry, that's my error. Matrix blocks don't have a handle attribute, i.e. it should be block.type, not block.handle in my examples above. I've amended the answer. Commented Dec 21, 2016 at 7:54
  • 1
    Awesome, it worked all perfectly.
    – Jon Riel
    Commented Dec 21, 2016 at 10:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.