twigphp / Twig

Twig, the flexible, fast, and secure template language for PHP
https://twig.symfony.com/
BSD 3-Clause "New" or "Revised" License
8.17k stars 1.25k forks source link

conditionally define a block in twig 2 #2393

Closed matthbull closed 6 years ago

matthbull commented 7 years ago

hi all,

twig 2.1
symfony3

Ive asked this on SO, but the only answer I have sort of confirms my suspicions, but I'd just like to check with you guys if this is actually the case.

I want to conditionally define a block in a twig template, as in the example below:

called template

{% extends 'base.html.twig' %}
{% set something = true %}
{% if not something %}
    {% block left_sidebar %}
        <h2>sidebar rendered</h2>
    {% endblock %}
{% endif %}

base.html.twig

{% if block('left_sidebar') is defined %}
   {{ block('left_sidebar')|raw }}
{% else %}
    no block
{% endif %}

I have a use case where I dont actually want to define the block if its not needed (dynamic regions in the base template).

The problem is, the block always renders, regardless of a true/false result of the conditional check. But this only seems to happen if the template is extending another. Is this because of compile order? Or am I missing something.

Heres a fiddle to illustrate.

thanks in advance.

Matt

fabpot commented 7 years ago

Think of a block as the equivalent of a method in a class. A block cannot be dynamic.

matthbull commented 7 years ago

Interstingly though, if you look at the compiled output from twigfiddle, its resulted in this.

    protected function doDisplay(array $context, array $blocks = array())
    {
        // line 3
        $context["something"] = null;
        // line 5
        if ((($context["something"] ?? null) != null)) {
        }
        // line 1
        $this->parent->display($context, array_merge($this->blocks, $blocks));
    }

note that // line 1 has compiled outside of the line 5 if statement?

Ive also noticed that SonataAdminBundle uses the same syntax, so maybe theres a general lack of understanding in the community?

fabpot commented 7 years ago

I think I will change Twig to throw an exception in such circumstances as it cannot work and people keep having this issue. The fact that it "fails" silently does not help. Any link to an example on SonataAdminBuindle?

javiereguiluz commented 7 years ago

Given that in the base template you don't make anything with the dynamic block, you can do the following:

  1. Define a main block in base.html.twig to hold the main contents of the page, including the optional left sidebar:
{% block main %}
{% endif %}
  1. In the child templates, define the left_sidebar when needed:
{% extends 'base.html.twig' %}

{% block main %}

    ...

    {% set something = true %}
    {% if not something %}
        {% block left_sidebar %}
            <h2>sidebar rendered</h2>
        {% endblock %}
    {% endif %}
{% endblock %}

But, if you don't really do anything with this left_sidebar block in any other template ... you can remove it altogether:

{% extends 'base.html.twig' %}

{% block main %}

    ...

    {% set something = true %}
    {% if not something %}
        <h2>sidebar rendered</h2>
        ...
    {% endif %}
{% endblock %}

Finally, remember that newer Twig versions allow you to check for block existence:

{# does the block exist in this template? #}
{% if block('left_sidebar') is defined %} {{ block('left_sidebar')|raw }} {% endif %}

{# does the block exist in another template? #}
{% if block('left_sidebar', 'base.html.twig') is defined %} {{ block('left_sidebar', 'base.html.twig')|raw }} {% endif %}
matthbull commented 7 years ago

oo thats an interesting idea Javiere, many thanks.

Fabian, heres the link to the sonata file. Its right at the bottom on line 353.

matthbull commented 7 years ago

another thing that might be relevant.

If I move the conditional statement that defines the block into the base template, the rendered PHP is this:

   // line 5
    if ((($context["something"] ?? null) != null)) {
        // line 6
        echo "    ";
        $this->displayBlock('left_sidebar', $context, $blocks);
    }

note the displayBlock call is in the right place. So this is only occurring in an extended template.

stof commented 7 years ago

@matthbull this is because the child template is not displaying the block. It is overwriting a parent block, which will be displayed in the place where the parent wants to display it.

And blocks can never be defined conditionally ({% if %} is a runtime conditional, while blocks are defined at compilation time)

matthbull commented 7 years ago

thanks @stof, thats what I was thinking was the case, but it was really unclear as it was silently failing, as @fabpot had already mentioned.

ricardopiresweb commented 6 years ago

Old but gold.

Had a similar need here. I wanned to show, by default, a base layout, but if user was not logged in, the login template should override the base template. Tried the same code that @matthbull, but didn't worked like a wanned.

Then, instead of extending the base layout in my index template, I've just included each template. Here's my before/after:

BEFORE (not working code)

{% extends "layout.twig" %}
{% if not var_loggedin and var_needlogin %}
    {% block title %}My System Login{% endblock %}
    {% block body %}
        {{ include("login.twig") }}
    {% endblock %}
{% endif %}

In the code above, the template always shown the login page, even if I was already logged in.

AFTER (now working OK ;D)

{% block page %}
{% if not loggedin and needlogin %}
    {{ include("login.twig") }}
    {% else %}
        {{ include("layout.twig") }}
    {% endif %}
{% endblock %}

Now, the login template is shown just if I'm not logged in and the page requires user to login, which is what I need.

Sorry for topic ressurrecting, but I'm new to template engines (about 3 days for now playing on Twig), and Google seems not that friendly as other subjects when the search is about Twig.

Hope this helps anyone else.

ChangePlaces commented 6 years ago

As this appears high in a search, if you need a dynamic block - instead of showing an empty block with no content, you could always use css to display:none

StijnVrolijk commented 6 years ago

I came across the following scenario:

{% if block('my_block') is defined %}
  <div class="some-extremely-obvious-css-inducing-class">
    {{ block('my_block') }}
  </div>
{% endif %}

With my extending template having

{% block my_block %}
  {% if myConditional %}
    foo
  {% endif %}
{% endblock %}

So if my conditional fails we don't see foo but we do see the extremely obvious surrounding div. To circumvent this me and my colleagues came up with the following solution:

{% if block('my_block') is defined and block('my_block') != '' %}
  <div class="some-extremely-obvious-css-inducing-class">
    {{ block('my_block') }}
  </div>
{% endif %}
{%- block my_block -%}
  {% if myConditional %}
    foo
  {% endif %}
{%- endblock -%}

We made the block spaceless by adding - to the tags and we used that to check if the block is empty in the parent template.

Not a very clean solution but it's cleaner than anything else we could come up with.

dylanjameswagner commented 3 years ago

I tried this out today and it appears to be working for me. I think my Twig version is 2.14.3

Using similar code to StijnVrolijk's example...

parent template layout.twig, partial, note the block('my_block')|trim != ''

{% if block('my_block') is defined and block('my_block')|trim != '' %}
  <div class="some-extremely-obvious-css-inducing-class">
    {{ block('my_block')|raw }}
  </div>
{% endif %}

child template

{% extends "layout.twig" %}
{% block my_block %}
  {% if myConditional %}
    foo
  {% endif %}
{% endblock %}