Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponents] Component HTML contains 11 elements, but only 1 root element is allowed, only when using nested components. #2312

Open
gremo opened this issue Oct 31, 2024 · 8 comments
Labels
LiveComponent Status: Waiting Feedback Needs feedback from the author

Comments

@gremo
Copy link
Contributor

gremo commented Oct 31, 2024

My PayrollSummary component is essentially an HTML table with rows populated from a database. There are 900 or more rows without any filters applied.

Long story short, I have a row cell with buttons:

<td>
    <twig:Button
        size="xs"
        theme="ghost"
    >
        <twig:ux:icon name="mdi:wand" class="size-4" />
    </twig:Button>

    <twig:Button> <!-- this is causing the error -->
        <twig:ux:icon name="mdi:edit" class="size-4" />
    </twig:Button>
</td>

This Twig excerpt will cause the following error:

Component HTML contains 11 elements, but only 1 root element is allowed, only when using nested components.

As soon as I change the second button to a simple HTML markup without a component, the problem disappear:

<button class="btn btn-xs btn-ghost">
    <twig:ux:icon name="mdi:edit" class="size-4" />
</button>

I've stripped all the props and attributes from my Button component to debug this error. I ended up with:

<button>
    {% block content %}{% endblock %}
</button>

Still getting the error. Any Idea?

EDIT: adding a random id doens't help:

<twig:Button
    id="payroll-{{ random() }}-justificatives-button"
    size="xs"
    theme="ghost"
    onclick="payroll_{{ payroll.id }}_justificatives.showModal()"
>
    <twig:ux:icon name="mdi:wand" class="size-4" />
</twig:Button>

<twig:Button
    id="payroll-{{ random() }}-edit-button"
    size="xs"
    theme="ghost"
>
    <twig:ux:icon name="mdi:edit" class="size-4" />
</twig:Button>
@smnandre
Copy link
Member

@gremo : is your button an anonymous or a class-based component ?

I'm not sure what happens here, the error mentionned occurs when a live component has not a unique root div with its attributes.

And can you show the rest of this template ?

@gremo
Copy link
Contributor Author

gremo commented Oct 31, 2024

@smnandre anonymous one.

This is driving me crazy. Facts:

  1. HTML markup is of course valid
  2. It happens even if the component's HTML is empty
  3. It happens only when a lot of records are printed, and with a certain number of components in the page

Right now I've 900 records and 3 components in this template. I've removed the components one by one checking for the errors. This is the minimal example that trigger the error. Note that twig:Button and twig:Badge are empty anonymous components, just in case.

If I remove any of the component of the below template, it starts working again:

<div {{ attributes }}>
    <table>
        <tbody>
            {% for record in computed.summary.records %}
                <tr>
                    <td>
                        <twig:Badge />
                    </td>
                    <td>
                        <twig:Button />
                        <twig:Button />
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
</div>

image

I can't imagine why this is happening and why it's related to the loop. I can't see any error or memory issue. It doesn't happen with {% for i in 1..900 %} by the way.

@smnandre
Copy link
Member

I wonder if your badge or button has not something in it that is rendered differently by the Browser DOM.

Does it occurs if you render only one line ?

If your records are dynamic you probably should add ids on the rows

@gremo
Copy link
Contributor Author

gremo commented Oct 31, 2024

@smnandre

I wonder if your badge or button has not something in it that is rendered differently by the Browser DOM.

I'm not sure I understand what you mean but they are actually totally empty templetes, just in case.

Does it occurs if you render only one line ?

{% for record in computed.summary.records[0:1] %} works. Even with a lower value like 700 it works!

If your records are dynamic you probably should add ids on the rows

Already tested with <tr id="row-{{ record.payroll.id }}"> and it doesn't help.

@smnandre
Copy link
Member

Even with a lower value like 700 it works!

I'm not suggesting the loop does not work, i'm trying to understand why the LiveComponent does not get the HTML ...

Maybe could you set a "if" around just to eliminate something ?

<div {{ attributes }}>
    <table>
        <tbody>
-          {% for record in computed.summary.records|slice(0, 1) %}
+          {% for record in computed.summary.records|[:1] %}
                <tr>
                    <td>
                        <twig:Badge />
                    </td>
                    <td>
                        <twig:Button />
                        <twig:Button />
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
</div>

they are actually totally empty templetes, just in case.

I'm not sure here... how do you see the bug if they are empty. I mean... what is triggering the LiveComponent re-render ?

Last thing, could you share the HTML generated (with this ultra-light template), i can then use it to create a small reproducer and try to understand what's happening

@smnandre
Copy link
Member

Looking at the error message in your capture i can confirm the HTML generated by the component during the re-render is not good (and contains too many things).

There 100% can be a LiveComponent bug here but yeah, i'll need more inforamtion.

(you can poke me on Slack if you want some privacy about this project - even when i'm not marked as "online" i'm often around ;) )

@gremo
Copy link
Contributor Author

gremo commented Oct 31, 2024

I’m happy to help and try to identify the bug. I’m not sure if I’ll be able to create a minimal reproducer, as there are a lot of objects and queries involved. Additionally, I'll be on holiday soon (tomorrow) and won’t have access to a computer.

Essentially, the summary variable is just a large DTO with some nested objects and arrays. The error occurs on the initial render (i.e., the first page refresh) of the Payroll component. Here’s the partially generated HTML (I doubt it can help):

<html lang="it">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <link rel="stylesheet" href="/build/embed/app.css">
    <script src="/build/embed/runtime.js" defer=""></script>
    <script src="/build/embed/vendors-node_modules_symfony_stimulus-bridge_dist_index_js.js" defer=""></script>
    <script src="/build/embed/app.js" defer=""></script>
  </head>
  <body class="p-4 space-y-4">
    <div data-controller="live" data-live-name-value="Embed:Payroll" data-live-url-value="/_components/Embed:Payroll" id="live-3359728119-0" data-live-query-mapping-value="{&quot;filters&quot;:{&quot;name&quot;:&quot;filters&quot;}}" data-live-props-value="{&quot;profile&quot;:3,&quot;filters&quot;:{&quot;month&quot;:9,&quot;userId&quot;:null},&quot;@attributes&quot;:{&quot;id&quot;:&quot;live-3359728119-0&quot;},&quot;@checksum&quot;:&quot;g+2Ts83YomFUFE2mFt\/QcN+bJGzxuCLNvyaIyhCCwTE=&quot;}" data-action="live:connect->live#$render"></div>
  </body>
</html>

An this is the component Payroll that gives the error:

#[AsLiveComponent(csrf: false)]
class Payroll
{
    use ComponentBusTrait;
    use DefaultActionTrait;

    #[LiveProp]
    public AppProfile $profile;

    #[LiveProp(url: true)]
    public PayrollFilters $filters;

    public function __construct()
    {
        $this->filters = new PayrollFilters();
    }

    #[ExposeInTemplate]
    public function getSummary(): PayrollSummary
    {
        return $this->handle(new BuildPayrollSummaryMessage(
            profile: $this->profile,
            startDate: $this->filters->toDatePeriod()->getStartDate(),
            endDate: $this->filters->toDatePeriod()->getEndDate(),
            userId: $this->filters->userId,
        ));
    }
}

@smnandre
Copy link
Member

smnandre commented Nov 1, 2024

If you can show me the AJAX response that triggered the error that'd help.

There may be something odd between loading="defer", url:true (not writable ?) and exposeInTemplate.

Side note: as you use loading=defer, the "exposeInTemplate" attribute should probably be avoided (it will load your 9000 rows... without rendering them during first page load (before re-render))

@smnandre smnandre added Status: Waiting Feedback Needs feedback from the author LiveComponent labels Nov 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LiveComponent Status: Waiting Feedback Needs feedback from the author
Projects
None yet
Development

No branches or pull requests

2 participants