acalvino4/twig-embed-implicit-default

Extends the twig embed tag to allow an implicit default block

1.0.1 2023-05-04 19:29 UTC

This package is auto-updated.

Last update: 2024-11-12 02:02:17 UTC


README

License Build Status

Test Coverage Phpstan Level Easy Coding Standard

TLDR

{% embed 'wrapper.twig' %}
  Default block content
{% endembed %}

{# wrapper.twig #}
<div class='bg-slate-300'>
  <div class='max-w-7xl py-16 '>
    <p>{% block default '' %}</p>{# --> renders "Default block content" #}
  </div>
</div>

Warning This extension introduces a breaking change to twig. However, it will most likely not affect too many of your files, is probably quite easy to work around, and is most definitely worth it. See details.

Contents

  1. Rationale
  2. Usage
  3. Getting Started

Rationale

In vanilla twig, to override a slot of a parent template via an embed, we need to declare a block.

{% embed 'wrapper.twig' %}
  {% block content %}
    <p>This is default block content</p>
  {% endblock %}
{% endembed %}

{# example wrapper.twig #}
<div class='bg-slate-300'>
  <div class='max-w-7xl py-16 '>
    {% block content %}{% endblock %}
  </div>
</div>

When following best practices to keep code modular and dry, you'll typically create components for various code chunks, but this results in rather bloated and unreadable markup, especially as you start to nest (and this is only two levels!):

{% embed 'wrapper.twig' %}
  {% block default %}
    <p>
      This is a CTA, so naturally it includes a
      {% embed 'button.twig' %}
        {% block default %}Button{% endblock %}
      {% endembed %}
    </p>
  {% endblock %}
{% endembed %}

The explicit block declarations are handy for extending layouts (where you'll need 'header', 'footer', and 'sidebar', for example), but in most cases, embed is probably just being used to include components where you need to pass in some content, and for these cases, typically only one content slot is required.

So we've sacrificed simplicity of the base case for greater flexibility ... but what if we could have both?

Usage

This twig extension allows the use of an implicit default block in embeds:

{% embed 'wrapper.twig' %}
  <p>This is default block content</p>
{% endembed %}

{# same as this, which still will work #}
{% embed 'my_subtemplate.twig' %}
  {% block default %}
    <p>This is default block content</p>
  {% endblock %}
{% endembed %}

The longer example above could become

{% embed 'wrapper.twig' %}
    <p>
      This is a CTA, so naturally it includes a
      {% embed 'button.twig' %}Button{% endembed %}
    </p>
{% endembed %}

To specify where that default content goes in the component template, follow the wrapper.twig example above, or, if your default block has no/simple default content, you can use twig's block shortcut syntax:

{% block default %}{% endblock %}
{# same as #}
{% block default '' %}

This works because all this extension does is parse the initial content within the embed into a block named 'default'.

Notes

The implicit default content can be combined with (any number of) named blocks:

{% embed 'wrapper.twig' %}
  <p>This is default block content</p>
  {% block special_slot %}
    Special content
  {% endblock %}
{% endembed %}

{# example wrapper.twig #}
<div class='bg-slate-300'>
  <div class='max-w-7xl py-16 '>
    {% block default '' %}
    {% block special_slot '' %}
  </div>
</div>

The implicit default content needs to be placed before any explicit blocks (See this issue):

{# Will throw error: "A template that extends another one cannot include content outside Twig blocks." #}
{% embed 'wrapper.twig' %}
  {% block special_slot %}
    Special content
  {% endblock %}
  <p>This is default block content</p>
{% endembed %}

If you have both implicit default content and an explicit default block, an error will be thrown (just as when you declare two blocks with the same name):

{# Will throw error: "The block 'default' has already been defined" #}
{% embed 'wrapper.twig' %}
  <p>This is default block content</p>
  {% block default %}
    Other default content???
  {% endblock %}
{% endembed %}

Breaking Change

Twig, by default, does not allow content that outputs within embed tags but outside block tags. In this respect, this extension doesn't change any defined behavior; it only defines what previously was an error. However, twig does allow non-outputting content in such a context, for example:

{% embed 'wrapper.twig' %}
  {% set a = 'foo bar' %}
  {% block content %}
    <p>{{ a }}</p>
  {% endblock %}
{% endembed %}

Using this extension, that set tag would get absorbed as part of the default block, and a would be undefined in the following block.

In this case, the solution is simple: move the set tag outside the embed scope. I suspect most instances of this change will be equally straightforward to adapt for this extension. If you run into something more complex, please file an issue and I will try to deal with or document that case.

Getting Started

composer require acalvino4/twig-embed-implicit-default

Then register the extension as appropriate for your framework.

Craft CMS

Since I am primarily a Craft developer, I created a plugin that wraps this package, so including it in your site is as easy an any other plugin.

composer require acalvino/craft-embed-implicit-default
./craft plugin/install embed-implicit-default

Or just find it on the plugin store and install from there.