loading...

Emulating Components in Twig

jakedohm_34 profile image Jake Dohm ・4 min read

Components are one of my favorite things about JavaScript frameworks like Vue.js. They allow you to follow DRY principles by removing repetition of code in your markup, and they provide a fantastic developer experience. When I work in Twig templates, not having access to bona fide components is definitely one of the things I miss most. Because of that, I've been looking for a solution to this problem for a while.

Before I talk about the solutions I tried out, and the one I've landed on, let me first give you my criteria for what features I need out of components, or a component replacement.

Criteria

  • Props: I should be able to pass named parameters (usually referred to as "props") into my component.
  • Slots: I should be able to pass chunks of markup (HTML, text, whatever) into my component, and the component should determine what to do with that markup (i.e. where to render it).

If you have experience with Vue.js or another component-based framework, these concepts should be very familiar to you. If not, you'll have to trust me that once you've used these component features, trying to write markup without them is painful 😂

Explored Solutions

There were two main Twig features that I thought I could use to achieve component-like functionality within Twig: Macros and Includes.

Macros

So I started experimenting with Macros. If you're not familiar, Macros are the "functions" of Twig. Here's an example Macro (see more in the Twig docs):

{% import _self as components %}

{% macro button(props) %}
    <button>{{ props.content }}</button>
{% endmacro %}

<p>{{ components.button({ content: 'Click Me' }) }}</p>

Using Macros satisfied one criterion, passing props, but it didn't solve my problem of wanting to pass markup into my "component" via named slots.

Includes

Next, I tried Twig's Includes. Includes are a way to break up your code into multiple files, and I often use them to break up my page templates so they're smaller and to make things easier to search and find. But, they have a built-in feature, "with", that allows you to pass context (i.e. variables) into the include which made me hopeful that I could hack them into working well as components!

But again, they only satisfied the first criteria. There was no (built-in) way to pass in chunks of markup to be rendered within the component's markup.

Side note: Technically, for both Macros or Includes, I could do something hacky like assigning a variable to a bunch of markup and passing that variable in, but that felt very wrong and un-intuitive.

Final Solution

After giving up hope, I eventually stumbled upon Twig's Embeds. I read the documentation, and instantly got excited because Embeds seemed to satisfy both criteria right out of the box! I immediately coded up an example on TwigFiddle (twigfiddle.com) and I found that Embeds fully met both of my criteria: Using the "with" functionality and "blocks" I could pass props and chunks of markup into the embed.

Here's an example of embed declaration, and its use:

{% embed "page.twig" with {
    social: ['facebook', 'twitter', 'snapchat']
} %}
    {% block header %}
        <div>Header Content</div>
    {% endblock %}

    {% block main %}
        <div>Main Content</div>
    {% endblock %}
{% endembed %}
{# File: page.twig #}

<div class="page-component">
    <header>{% block header %}Default Header Content{% endblock %}</header>

    <main>{% block main %}Default Main Content{% endblock %}</main>

    <footer>{% block footer %}Default Footer Content{% endblock %}</footer>

    {% if social is defined %}
        <ul class="social">
            {% for item in social %}<li>{{ item }}</li>{% endfor %}
        </ul>
    {% endif %}
</div>

This method of emulating components in Twig isn't without its drawbacks, but I think it works well for most scenarios. Here are a few notes about the pros and cons of embeds:

Pros

  • You can pass in both props (variables) and slots (blocks).
  • You can specify default content for each block.
  • Using the "with only" feature, you can encapsulate your component so it won't implicitly receive any variables from the context in which it's used.

Cons

  • You have to specifically reference the embed's file path in every usage
  • You do have to use the "only" keyword to make sure your parent template's variables don't leak into the component.

Final Thoughts

I don't think Embeds are a fantastic long-term solution, and I'd love to have component functionality natively in Twig, but I was happy to find something that satisfies my needs and allows me to clean up my templates in the way that I'm comfortable with.

I'm also intrigued by Twig Components Bundle, a Symfony package that appears to add component functionality to Twig, based on the component system of Vue.js. But, I haven't tried it out yet. I'll make sure to report back if I do!


I work for an awesome company called Good Work. We are an expert team of web developers helping design teams at agencies, brands and startups build things for web and mobile.

If you're looking for someone to work with on your web projects, don't hesitate to reach out!

Posted on Jan 22 '19 by:

Discussion

markdown guide
 

I love using embeds, but there's another drawback you should be aware of:

When using any macros within those blocks you need to add the import within the embed.
If you don't then you get a real obscure error that doesn't leave many clues as to what's going on.

{# Import as normal #}
{% import 'macros/index.twig' as macro %}
{{ macro.makeMeASandwich }}

{% embed "page.twig" with {} %}
    {# Have to import again as the import above isn't in scope here #}
    {% import 'macros/index.twig' as macro %}

    {% block main %}
        <div>{{ macro.makeMeASandwich }}</div>
    {% endblock %}

{% endembed %}
 

Thanks for mentioning this!

I actually like having to explicitly import embeds, so it's not a problem for me, but it's definitely a good thing to note 😄

 

No problem!

I'd enjoy twig imports more if they acted like Javascript imports.

In this instance you're forced away from the common practice of declaring them all at the top of the page and instead be within the embed tag.

No deal breakers though, embeds are great.

 

Using Macros satisfied one criterion, passing props, but it didn't solve my problem of wanting to pass markup into my "component" via named slots.

You can easily pass whole chunks of HTML to a macro by storing that HTML in a variable, pass it to the macro and output with the raw filter.

To define a variable storing markup, you can use an extented form of the set command e.g.

{% set slotContent %}
  <h1>My markup</h1>
{% endset %}

I don't know if I'm doing it right, but I used only macros to build a library of around 50 components I deliver frequently with updates to the back-end developers using some form of Symfony framework.

Also I used Patternlab Twig Edition to develop the components decoupled from the application that will use it.

 

Hey René!

Yeah, this solution totally works! Here are a few reasons I prefer using Embeds over this:

  1. Embeds are more readable: It's easier to read the {% block %} syntax within an embed than it is to see one (or multiple) statements setting HTML to a variable, and then passing those variables into a Macro.
  2. The syntax for outputting the blocks of content (within the embed declaration, is cleaner with embeds. This is especially true if you want to set defaults.

There may be other reasons, but these are the ones that come to mind.

All of that said, the way you're doing it isn't "wrong" and I'm glad you found a way to accomplish what you needed to!

 

Hey Jake, I'm curious what you think about our approach: github.com/redantnl/twig-components. I've published an article in php|architect a few months ago with some background on this approach.

 

Hey Gert, thanks for asking! That's a really interesting approach. The main thing it's missing IMO is the ability to pass chunks of markup into the component like you can do with Embeds. That's the main gist of the article, pushing to use Embeds instead of Includes.

I love the prop checking, that's a neat feature!

Overall, I think you're on to something and I'd love to see it further fleshed out 😄

 

Ah, interesting thought about passing markup! We do actually do that occassionally. for 'container'-like components:

{# Add all tabs to a tabs bar ... #}

{{ render_component('ui.tabs', {
    items: [ 'general' ]
}) }}

{# Render the tab contents ... #}

{{ render_component('ui.tab', {
    id: 'general',
    content: include('@Sales/partial/general.html.twig', { 'order': salesOrder })
}) }}

{# Render more tabs ... #}

with the Tab-component implementation looking like this:

{% component tab {
    id:      { type: 'string',
               comment: 'Tab id (match with tabs)',
               preview: 'tab_one' },
    content: { type: 'string',
               comment: 'Your tab contents' }
} with options %}

<div class="ui tab" data-tab="{{ tab.id }}">
    {{ tab.content|raw }}
</div>

Although that of course uses Includes instead of Embeds...

You could, for instance, include a render_component() or block() function call inside a component, like this:

    {{ render_component('ui.tab', {
        id: datatable.name,
        content: render_component('ui.datatable', {
            data_table: datatable
        })
    }) }}

Is this sort of what you were thinking of? Do you have any suggestions for further fleshing out? Would love to see our approach find some traction 😉