ux icon indicating copy to clipboard operation
ux copied to clipboard

[TwigComponent] Having troubles accessing blocks in deeply nested component

Open gremo opened this issue 4 months ago • 11 comments

I'm using TailwindCSS so reusing classes is vital.

My component Layout:Default (see the definition below) can be used inside a page:

<twig:Page title="Dashboard">
    <twig:Layout:Default>
        {# <main> content #}
    </twig:Layout:Default>
</twig:Page>

I want the content of Layout:Default to be put directly inside Layout:Main component. I can achieve this using layoutContent variable:

{# components/Layout/Default.html.twig #}

<twig:Layout:Wrapper>
    <twig:Layout:Header />
    {% with { layoutContent: outerBlocks.content } %}
        <twig:Layout:Main>
            {{ block(layoutContent) }}
        </twig:Layout:Main>
    {% endwith %}
    <twig:Layout:Footer />
</twig:Layout:Wrapper>

...but I can't find a way to avoid this variable and accessing the block using outerBlocks context:

{{ block(outerBlocks.content) }}

An exception has been thrown during the rendering of a template ("Xdebug has detected a possible infinite loop, and aborted your script with a stack depth of '512' frames").

{{ block(outerBlocks.outerBlocks.content) }}

Impossible to access an attribute ("content") on a string variable ("outer__block_fallback").

Just out of curiosity, what I'm doing wrong here?

gremo avatar Feb 15 '24 18:02 gremo

What does your twig:Layout:Wrapper does ?

If you used a simple div there and not a component you could simply do this

{# template/dashboard.html.twig #}

<twig:Page title="Dashboard">
    <twig:Layout:Default>
        {# everything here is forwarded to the "content" block of the component #}
        {# <main> content #}
    </twig:Layout:Default>
</twig:Page>
{# template/components/Layout/Default.html.twig #}

<div class="wrapper">

    <twig:Layout:Header />

    <twig:block name="content">
          {# <main> content will appear here, as the default content block for this component #}
    </twig:block>
    
    <twig:Layout:Footer />
</div>

smnandre avatar Feb 15 '24 19:02 smnandre

@smnandre first thank you for helping! I really appreciate.

I already have a solution I posted, so the question was really about how to get the content block using outerScope/outerBlocks.

By the way, the Layout:Wrapper right now is very simple. But the question remains and apply to other cases too (when you have deeply nested components).

<div
    class="{{ html_classes(
        "flex flex-col min-h-svh font-roboto",
        attributes.all.class|default(""),
    ) }}"
    {{ attributes.without('class') }}>
    {% block content %}{% endblock %}
</div>

gremo avatar Feb 15 '24 20:02 gremo

Today the TwigComponent HTML syntax is "just a syntax".. the inner working are still based on Twig...

... but Twig is more designed to work in the other way with the extends/embed/use system

So i don't see an immediate solution for you there to pass content to N level down sorry :|

smnandre avatar Feb 15 '24 22:02 smnandre

I'm also interested in that.

seb-jean avatar Mar 07 '24 16:03 seb-jean

@sneakyvv Does any solution or problem strike you?

weaverryan avatar Mar 07 '24 16:03 weaverryan

I eventually opted for a much simpler design, avoiding nesting more than two levels deep. Problem solved, much clarity achieved.

Feel free to close this issue in order to not pollute this repo.

gremo avatar Mar 07 '24 19:03 gremo

For my part, I encounter this problem.

seb-jean avatar Mar 07 '24 19:03 seb-jean

@gremo I was also wondering what the other components looked like as @smnandre asked, but good it got solved, even though via a workaround/simplification

@seb-jean What is your specific case? Gut feeling is that it might be something related to a setup described in this test: https://github.com/symfony/ux/blob/2.x/src/TwigComponent/tests/Integration/EmbeddedComponentTest.php#L128-L139

sneakyvv avatar Mar 08 '24 07:03 sneakyvv

I just removed one level as @smnandre suggested:

{# template/components/Page/Base.html.twig #}
{%- props title = null -%}

<!DOCTYPE html>
<html lang="{{ app.request.locale }}" {{- attributes.without("lang", "class") -}}>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        {% block meta %}{% endblock %}
        {% if title %}
            <title>{{ title }}</title>
        {% endif %}

        {% block stylesheets %}
            {{ encore_entry_link_tags('app') }}
        {% endblock %}

        {% block javascripts %}
            {{ encore_entry_script_tags('app') }}
        {% endblock %}
    </head>
    <body {{- attributes.only("class") -}}>
        {% block content %}{% endblock %}
    </body>
</html>

And then:

{# templates/components/Layout/Default.html.twig #}
{%- props title -%}

<twig:Page:Base :title="title" class="font-roboto-flex">
    <div class="flex flex-col min-h-svh">
        <header class="flex-shrink-0">
            {# Default header for all pages that use thhis layout #}
            {# or use {{ block(outerBlocks.header) }} #}
        </header>

        <main
            class="{{ html_classes(
                "flex-auto flex-shrink-0",
                outerScope.attributes.all.class|default("")
            )|trim }}"
            {{- outerScope.attributes.without("class", "title") -}}
        >
            {{ block(outerBlocks.content) }}
        </main>

        <footer class="flex-shrink-0 sticky bottom-0">
            {# Default footer for all pages that use thhis layout #}
            {# or use {{ block(outerBlocks.footer) }} #}
        </footer>
    </div>
</twig:Page:Base>

A normal page would just use <twig:Layout:Default>. Hope this works for you.

gremo avatar Mar 08 '24 07:03 gremo

@sneakyvv Below my files:

{# templates/components/Button.html.twig #}

<twig:Link>
    <twig:Button:TouchTarget>
        {{ block(outerBlocks.content) }}
    </twig:Button:TouchTarget>
</twig:Link>
{# templates/components/Link.html.twig #}

<a{{ attributes }}>
    {% block content %}{% endblock %}
</a>
{# templates/components/Button/TouchTarget.html.twig #}

{% block content %}{% endblock %}
<span class="absolute left-1/2 top-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 [@media(pointer:fine)]:hidden" aria-hidden="true"></span>

seb-jean avatar Mar 08 '24 08:03 seb-jean

@seb-jean I may be wrong but if you want to refer to block content passed to Link, and pass it again to TouchTarget, you must store in a variable, since it's 2-level deep:

{# templates/components/Button.html.twig #}

<twig:Link>
    {% with { content: outerBlocks.content } %}
        <twig:Button:TouchTarget>
            {{ block(content) }}
        </twig:Button:TouchTarget>
    {% endwith %}
</twig:Link>

gremo avatar Mar 08 '24 09:03 gremo