Skip to content

Commit 8f82680

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 8f82680

File tree

10 files changed

+87
-32
lines changed

10 files changed

+87
-32
lines changed

src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
102102
$attributesCollection = $attributesCreator->attributesForRendering(
103103
$mounted,
104104
$metadata,
105-
$this->componentStack->hasParentComponent()
106105
);
107106

108107
return new ComponentAttributes($attributesCollection->toEscapedArray());

src/LiveComponent/src/Util/ChildComponentPartialRenderer.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ public function renderChildComponent(string $deterministicId, string $currentPro
5959
$attributesCollection = $this->getAttributesCreator()->attributesForRendering(
6060
$mounted,
6161
$this->getComponentFactory()->metadataFor($componentName),
62-
true,
6362
$deterministicId
6463
);
6564

src/LiveComponent/src/Util/LiveControllerAttributesCreator.php

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function __construct(
5454
* Calculates the array of extra attributes that should be added to the root
5555
* component element to activate the live controller functionality.
5656
*/
57-
public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, bool $isChildComponent, string $deterministicId = null): LiveAttributesCollection
57+
public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, string $deterministicId = null): LiveAttributesCollection
5858
{
5959
$attributesCollection = $this->attributeHelper->create();
6060
$attributesCollection->setLiveController($mounted->getName());
@@ -96,14 +96,12 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad
9696
$mountedAttributes = $mountedAttributes->defaults(['data-live-id' => $id]);
9797
}
9898

99-
if ($isChildComponent) {
100-
$fingerprint = $this->fingerprintCalculator->calculateFingerprint(
101-
$mounted->getInputProps(),
102-
$this->metadataFactory->getMetadata($mounted->getName())
103-
);
104-
if ($fingerprint) {
105-
$attributesCollection->setFingerprint($fingerprint);
106-
}
99+
$fingerprint = $this->fingerprintCalculator->calculateFingerprint(
100+
$mounted->getInputProps(),
101+
$this->metadataFactory->getMetadata($mounted->getName())
102+
);
103+
if ($fingerprint) {
104+
$attributesCollection->setFingerprint($fingerprint);
107105
}
108106

109107
$dehydratedProps = $this->dehydrateComponent(

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: 12 additions & 5 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));
@@ -77,7 +84,7 @@ public function render(MountedComponent $mounted): string
7784
}
7885

7986
public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
80-
{
87+
{
8188
$context[PreRenderEvent::EMBEDDED] = true;
8289

8390
$mounted = $this->factory->create($name, $props);

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: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,37 @@ 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)
4064
->raw(']->embeddedContext(')
4165
->string($this->getAttribute('component'))
42-
->raw(', twig_to_array(')
66+
->raw(', ')
67+
->raw('twig_to_array(')
4368
->subcompile($this->getNode('variables'))
4469
->raw('), ')
4570
->raw($this->getAttribute('only') ? '[]' : '$context')
@@ -49,7 +74,7 @@ public function compile(Compiler $compiler): void
4974
->raw($this->getAttribute('index'))
5075
->raw(");\n")
5176
;
52-
77+
5378
$compiler->write('$embeddedBlocks = $embeddedContext[')
5479
->string('outerBlocks')
5580
->raw(']->convert($blocks, ')
@@ -60,5 +85,11 @@ public function compile(Compiler $compiler): void
6085
$this->addGetTemplate($compiler);
6186
$compiler->raw('->display($embeddedContext, $embeddedBlocks);');
6287
$compiler->raw("\n");
88+
89+
$compiler
90+
->outdent()
91+
->write('}')
92+
->raw("\n")
93+
;
6394
}
6495
}

0 commit comments

Comments
 (0)