Skip to content

Commit 69aefec

Browse files
committed
Allow embedded live components to be short-circuited just like non-embedded components
only re-rendering them when a LiveProp changed that allows updates from the parent
1 parent 5e73d8c commit 69aefec

File tree

8 files changed

+88
-24
lines changed

8 files changed

+88
-24
lines changed

src/LiveComponent/tests/Fixtures/templates/components/todo_list.html.twig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
{% if includeDataLiveId %}
88
{% set componentProps = componentProps|merge({'data-live-id': ('todo-item-' ~ loop.index) }) %}
99
{% endif %}
10-
{{ component('todo_item', componentProps) }}
10+
{% if loop.index is odd %}
11+
{{ component('todo_item', componentProps) }}
12+
{% else %}
13+
{% component 'todo_item' with componentProps %}{% endcomponent %}
14+
{% endif %}
1115
{% endfor %}
1216
</ul>
1317
</div>

src/LiveComponent/tests/Fixtures/templates/components/todo_list_with_keys.html.twig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
<ul>
55
{% for key, item in items %}
6-
{{ component('todo_item', { text: item.text, textLength: item.text|length, key: 'the-key'~key }) }}
6+
{% if loop.index is odd %}
7+
{{ component('todo_item', { text: item.text, textLength: item.text|length, key: 'the-key'~key }) }}
8+
{% else %}
9+
{% component 'todo_item' with { text: item.text, textLength: item.text|length, key: 'the-key'~key } %}{% endcomponent %}
10+
{% endif %}
711
{% endfor %}
812
</ul>
913
</div>

src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
2424
* The deterministic id of the "todo_item" components in todo_list.html.twig.
2525
* If that template changes, this will need to be updated.
2626
*/
27-
public const TODO_ITEM_DETERMINISTIC_PREFIX = 'live-289310975-';
27+
public const TODO_ITEM_DETERMINISTIC_PREFIX = 'live-1715058793-';
28+
public const TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED = 'live-2285361477-';
2829

2930
public function testInitLiveComponent(): void
3031
{
@@ -91,7 +92,7 @@ public function testItAddsIdAndFingerprintToChildComponent(): void
9192
$lis = $ul->children('li');
9293
// deterministic id: should not change, and counter should increase
9394
$this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'0', $lis->first()->attr('data-live-id'));
94-
$this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'2', $lis->last()->attr('data-live-id'));
95+
$this->assertSame(self::TODO_ITEM_DETERMINISTIC_PREFIX.'1', $lis->last()->attr('data-live-id'));
9596

9697
// the data-live-id attribute also needs to be part of the "props" so that it persists on renders
9798
$props = json_decode($lis->first()->attr('data-live-props-value'), true);

src/LiveComponent/tests/Functional/EventListener/InterceptChildComponentRenderSubscriberTest.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ final class InterceptChildComponentRenderSubscriberTest extends KernelTestCase
2626
// in buildUrlForTodoListComponent
2727
private static array $actualTodoItemFingerprints = [
2828
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'0' => 'dSQ4+SgsF3QWeK4ngSOM1ROM50s6N1kWAK6bYW2JjZU=',
29-
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1' => 'sMvvf7q68tz/Cuk+vDeisDiq+7YPWzT+WZFzI37dGHY=',
30-
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'2' => '8AooEz36WYQyxj54BCaDm/jKbcdDdPDLaNO4/49bcQk=',
29+
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED.'0' => 'sMvvf7q68tz/Cuk+vDeisDiq+7YPWzT+WZFzI37dGHY=',
30+
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1' => '8AooEz36WYQyxj54BCaDm/jKbcdDdPDLaNO4/49bcQk=',
3131
];
3232

3333
public function testItAllowsFullChildRenderOnMissingFingerprints(): void
@@ -74,6 +74,7 @@ public function testItRendersEmptyElementOnMatchingFingerprintWithCustomDataLive
7474
public function testItRendersNewPropWhenFingerprintDoesNotMatch(): void
7575
{
7676
$fingerprints = self::$actualTodoItemFingerprints;
77+
$fingerprints[AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED.'0'] = 'wrong fingerprint';
7778
$fingerprints[AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1'] = 'wrong fingerprint';
7879

7980
$this->browser()
@@ -93,11 +94,11 @@ public function testItRendersNewPropWhenFingerprintDoesNotMatch(): void
9394
), $content);
9495
// new props are JUST the "textLength" + a checksum for it specifically
9596
$this->assertStringContainsString(sprintf(
96-
'<li data-live-name-value="todo_item" data-live-id="%s1" data-live-fingerprint-value="sMvvf7q68tz&#x2F;Cuk&#x2B;vDeisDiq&#x2B;7YPWzT&#x2B;WZFzI37dGHY&#x3D;" data-live-props-value="&#x7B;&quot;textLength&quot;&#x3A;18,&quot;&#x40;checksum&quot;&#x3A;&quot;LGxXa9fMKrJ6PelkUPfqmdwnfkk&#x2B;LORgoJHXyPpS3Pw&#x3D;&quot;&#x7D;"></li>',
97-
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
97+
'<li data-live-name-value="todo_item" data-live-id="%s0" data-live-fingerprint-value="sMvvf7q68tz&#x2F;Cuk&#x2B;vDeisDiq&#x2B;7YPWzT&#x2B;WZFzI37dGHY&#x3D;" data-live-props-value="&#x7B;&quot;textLength&quot;&#x3A;18,&quot;&#x40;checksum&quot;&#x3A;&quot;LGxXa9fMKrJ6PelkUPfqmdwnfkk&#x2B;LORgoJHXyPpS3Pw&#x3D;&quot;&#x7D;"></li>',
98+
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED
9899
), $content);
99100
$this->assertStringContainsString(sprintf(
100-
'<li data-live-id="%s2"></li>',
101+
'<li data-live-name-value="todo_item" data-live-id="%s1" data-live-fingerprint-value="8AooEz36WYQyxj54BCaDm&#x2F;jKbcdDdPDLaNO4&#x2F;49bcQk&#x3D;" data-live-props-value="&#x7B;&quot;textLength&quot;&#x3A;10,&quot;&#x40;checksum&quot;&#x3A;&quot;BXUk7q6LI&#x5C;&#x2F;6Qx3c62Xiui6287YndmoK3QmVq6e5mcGk&#x3D;&quot;&#x7D;"></li>',
101102
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
102103
), $content);
103104
});
@@ -107,11 +108,13 @@ public function testItUsesKeysToRenderChildrenLiveIds(): void
107108
{
108109
$fingerprintValues = array_values(self::$actualTodoItemFingerprints);
109110
$fingerprints = [];
111+
$i = 0;
110112
foreach ($fingerprintValues as $key => $fingerprintValue) {
113+
$prefix = 0 !== $i++ % 2 ? 'live-4172682817-the-key' : 'live-521026374-the-key';
111114
// creating fingerprints keys to match todo_list_with_keys.html.twig
112-
$fingerprints['live-1745423312-the-key'.$key] = $fingerprintValue;
115+
$fingerprints[$prefix.$key] = $fingerprintValue;
113116
}
114-
$fingerprints['live-1745423312-the-key1'] = 'wrong fingerprint';
117+
$fingerprints['live-4172682817-the-key1'] = 'wrong fingerprint';
115118

116119
$urlSimple = $this->doBuildUrlForComponent('todo_list_with_keys', []);
117120
$urlWithChangedFingerprints = $this->doBuildUrlForComponent('todo_list_with_keys', $fingerprints);
@@ -122,12 +125,12 @@ public function testItUsesKeysToRenderChildrenLiveIds(): void
122125
->assertHtml()
123126
->assertElementCount('ul li', 3)
124127
// check for the live-id we expect based on the key
125-
->assertContains('data-live-id="live-1745423312-the-key0"')
128+
->assertContains('data-live-id="live-521026374-the-key0"')
126129
->assertNotContains('key="the-key0"')
127130
->visit($urlWithChangedFingerprints)
128-
->assertContains('<li data-live-id="live-1745423312-the-key0"></li>')
131+
->assertContains('<li data-live-id="live-521026374-the-key0"></li>')
129132
// this one is changed, so it renders a full element
130-
->assertContains('<li data-live-name-value="todo_item" data-live-id="live-1745423312-the-key1"')
133+
->assertContains('<li data-live-name-value="todo_item" data-live-id="live-4172682817-the-key1"')
131134
;
132135
}
133136

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,21 @@ public function __construct(
3939
) {
4040
}
4141

42-
public function createAndRender(string $name, array $props = []): string
42+
/**
43+
* Allow the render process to be short-circuited.
44+
*/
45+
public function preCreateForRender(string $name, array $props = []): ?string
4346
{
4447
$event = new PreCreateForRenderEvent($name, $props);
4548
$this->dispatcher->dispatch($event);
4649

47-
// allow the process to be short-circuited
48-
if (null !== $rendered = $event->getRenderedString()) {
49-
return $rendered;
50+
return $event->getRenderedString();
51+
}
52+
53+
public function createAndRender(string $name, array $props = []): string
54+
{
55+
if ($preRendered = $this->preCreateForRender($name, $props)) {
56+
return $preRendered;
5057
}
5158

5259
return $this->render($this->factory->create($name, $props));
@@ -84,13 +91,19 @@ public function embeddedContext(string $name, array $props, array $context, stri
8491
$mounted->addExtraMetadata('hostTemplate', $hostTemplateName);
8592
$mounted->addExtraMetadata('embeddedTemplateIndex', $index);
8693

87-
$embeddedContext = $this->preRender($mounted, $context)->getVariables();
94+
$this->componentStack->push($mounted);
8895

89-
if (!isset($embeddedContext['outerBlocks'])) {
90-
$embeddedContext['outerBlocks'] = new BlockStack();
91-
}
96+
try {
97+
$embeddedContext = $this->preRender($mounted, $context)->getVariables();
98+
99+
if (!isset($embeddedContext['outerBlocks'])) {
100+
$embeddedContext['outerBlocks'] = new BlockStack();
101+
}
92102

93-
return $embeddedContext;
103+
return $embeddedContext;
104+
} finally {
105+
$this->componentStack->pop();
106+
}
94107
}
95108

96109
private function preRender(MountedComponent $mounted, array $context = []): PreRenderEvent

src/TwigComponent/src/Test/InteractsWithTwigComponents.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ protected function mountTwigComponent(string $name, array $data = []): object
3030
/**
3131
* @param array<string,string> $blocks
3232
*/
33-
protected function renderTwigComponent(string $name, array $data = [], ?string $content = null, array $blocks = []): RenderedComponent
33+
protected function renderTwigComponent(string $name, array $data = [], string $content = null, array $blocks = []): RenderedComponent
3434
{
3535
if (!$this instanceof KernelTestCase) {
3636
throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ public function render(string $name, array $props = []): string
6161
}
6262
}
6363

64+
public function preRender(string $name, array $props): ?string
65+
{
66+
try {
67+
return $this->container->get(ComponentRenderer::class)->preCreateForRender($name, $props);
68+
} catch (\Throwable $e) {
69+
$this->throwRuntimeError($name, $e);
70+
}
71+
}
72+
6473
public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
6574
{
6675
try {

src/TwigComponent/src/Twig/ComponentNode.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ public function compile(Compiler $compiler): void
3434
{
3535
$compiler->addDebugInfo($this);
3636

37+
$compiler
38+
->write('$preRendered = $this->extensions[')
39+
->string(ComponentExtension::class)
40+
->raw(']->preRender(')
41+
->string($this->getAttribute('component'))
42+
->raw(', ')
43+
->raw('twig_to_array(')
44+
->subcompile($this->getNode('variables'))
45+
->raw(')')
46+
->raw(");\n")
47+
;
48+
49+
$compiler
50+
->write('if (null !== $preRendered) {')
51+
->raw("\n")
52+
->indent()
53+
->write('echo $preRendered;')
54+
->raw("\n")
55+
->outdent()
56+
->write('} else {')
57+
->raw("\n")
58+
->indent()
59+
;
60+
3761
$compiler
3862
->write('$embeddedContext = $this->extensions[')
3963
->string(ComponentExtension::class)
@@ -60,5 +84,11 @@ public function compile(Compiler $compiler): void
6084
$this->addGetTemplate($compiler);
6185
$compiler->raw('->display($embeddedContext, $embeddedBlocks);');
6286
$compiler->raw("\n");
87+
88+
$compiler
89+
->outdent()
90+
->write('}')
91+
->raw("\n")
92+
;
6393
}
6494
}

0 commit comments

Comments
 (0)