Skip to content

[LiveComponent] Nested components: Block content cannot access parent variables on re-render #3171

@melbings

Description

@melbings

Background

I’m trying to build a very simple Accordion Live Component that takes two content blocks - one for the collapsed state and one for the expanded state - and toggles between them.

I’m running into the limitation mentioned in the Live Component documentation:

Passing content via blocks to Live components works completely the same way you would pass content to Twig Components. Except with one important difference: when a component is re-rendered, any variables defined only in the "outside" template will not be available.

I’d like to confirm whether my understanding is correct and, if it is, whether there is a recommended pattern or if this is something that could be improved.

Minimal example

Accordion usage

{# any_file.html.twig #}

<twig:Accordion>
    <twig:block name="collapsed">
        Content to be shown when the accordion is collapsed.
    </twig:block>

    <twig:block name="expanded">
        Content to be shown when the accordion is expanded.
    </twig:block>
</twig:Accordion>

Accordion implementation

{# Accordion.html.twig #}

<div {{ attributes }}>
    {% if isCollapsed %}
        {{ block('collapsed') }}
   
        <button {{ live_action('expand') }}>Expand</button>
    {% else %}
        {{ block('expanded') }}

        <button {{ live_action('collapse') }}>Collapse</button>
    {% endif %}
</div>
// Accordion.php

#[AsLiveComponent]
class Accordion
{
    use DefaultActionTrait;

    public bool $isCollapsed = true;

    #[LiveAction]
    public function expand(): void
    {
        $this->isCollapsed = false;
    }

    #[LiveAction]
    public function collapse(): void
    {
        $this->isCollapsed = true;
    }
}

Problematic use case: Nested live components

Now imagine the Accordion is used inside another component that defines some data:

{# OuterComponent.html.twig #}

<div {{ attributes }}>
    {# Nesting another live component: #}
    <twig:Accordion>
        <twig:block name="collapsed">
            {{ someData }}
        </twig:block>
    
        <twig:block name="expanded">
            {{ someData }}
        </twig:block>
    </twig:Accordion>
</div>
// OuterComponent.php

#[AsLiveComponent]
class OuterComponent
{
    use DefaultActionTrait;

    #[LiveProp]
    public string $someData;
}

The initial rendering works, but any re-render results in an error message like Variable "someData" does not exist in OuterComponent.html.twig.

Expectations

I would like to...

  • ... let the outer component own the data
  • ... let the inner Accordion component own only the toggling behaviour
  • ... pass the content via blocks in a way that continues to work on re-renders

Work-around

So far, I only got this to work with a custom accordion_controller.js Stimulus controller that takes care of expanding/collapsing content via JavaScript.

Questions

  1. Is my understanding correct?
  2. Is there a recommended pattern for this kind of composition?
  3. Would it be possible (or desirable) to improve this? Might a solution be linked to the special outerScope and outerBlocks variables?

Conceptually, this feels like a very common pattern (accordion, tabs, etc.), but

From my perspective, being able to build a Live “container” component that still allows the outer component to own the data and layout is a very common use case (accordions, tabs, modals, ...). With the current behavior, it feels surprisingly hard to achieve.

Thanks a lot for any clarification or guidance :)

Previous discussion

#844 seems to be related.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LiveComponentRFCRFC = Request For Comments (proposals about features that you want to be discussed)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions