Skip to content

Commit

Permalink
Allow embedded live components to be short-circuited just like non-em…
Browse files Browse the repository at this point in the history
…bedded components

only re-rendering them when a LiveProp changed that allows updates from the parent
  • Loading branch information
sneakyvv committed Jun 26, 2023
1 parent 5e73d8c commit 8f82680
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ private function getLiveAttributes(MountedComponent $mounted, ComponentMetadata
$attributesCollection = $attributesCreator->attributesForRendering(
$mounted,
$metadata,
$this->componentStack->hasParentComponent()
);

return new ComponentAttributes($attributesCollection->toEscapedArray());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public function renderChildComponent(string $deterministicId, string $currentPro
$attributesCollection = $this->getAttributesCreator()->attributesForRendering(
$mounted,
$this->getComponentFactory()->metadataFor($componentName),
true,
$deterministicId
);

Expand Down
16 changes: 7 additions & 9 deletions src/LiveComponent/src/Util/LiveControllerAttributesCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function __construct(
* Calculates the array of extra attributes that should be added to the root
* component element to activate the live controller functionality.
*/
public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, bool $isChildComponent, string $deterministicId = null): LiveAttributesCollection
public function attributesForRendering(MountedComponent $mounted, ComponentMetadata $metadata, string $deterministicId = null): LiveAttributesCollection
{
$attributesCollection = $this->attributeHelper->create();
$attributesCollection->setLiveController($mounted->getName());
Expand Down Expand Up @@ -96,14 +96,12 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad
$mountedAttributes = $mountedAttributes->defaults(['data-live-id' => $id]);
}

if ($isChildComponent) {
$fingerprint = $this->fingerprintCalculator->calculateFingerprint(
$mounted->getInputProps(),
$this->metadataFactory->getMetadata($mounted->getName())
);
if ($fingerprint) {
$attributesCollection->setFingerprint($fingerprint);
}
$fingerprint = $this->fingerprintCalculator->calculateFingerprint(
$mounted->getInputProps(),
$this->metadataFactory->getMetadata($mounted->getName())
);
if ($fingerprint) {
$attributesCollection->setFingerprint($fingerprint);
}

$dehydratedProps = $this->dehydrateComponent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
{% if includeDataLiveId %}
{% set componentProps = componentProps|merge({'data-live-id': ('todo-item-' ~ loop.index) }) %}
{% endif %}
{{ component('todo_item', componentProps) }}
{% if loop.index is odd %}
{{ component('todo_item', componentProps) }}
{% else %}
{% component 'todo_item' with componentProps %}{% endcomponent %}
{% endif %}
{% endfor %}
</ul>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

<ul>
{% for key, item in items %}
{{ component('todo_item', { text: item.text, textLength: item.text|length, key: 'the-key'~key }) }}
{% if loop.index is odd %}
{{ component('todo_item', { text: item.text, textLength: item.text|length, key: 'the-key'~key }) }}
{% else %}
{% component 'todo_item' with { text: item.text, textLength: item.text|length, key: 'the-key'~key } %}{% endcomponent %}
{% endif %}
{% endfor %}
</ul>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ final class AddLiveAttributesSubscriberTest extends KernelTestCase
* The deterministic id of the "todo_item" components in todo_list.html.twig.
* If that template changes, this will need to be updated.
*/
public const TODO_ITEM_DETERMINISTIC_PREFIX = 'live-289310975-';
public const TODO_ITEM_DETERMINISTIC_PREFIX = 'live-1715058793-';
public const TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED = 'live-2285361477-';

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

// the data-live-id attribute also needs to be part of the "props" so that it persists on renders
$props = json_decode($lis->first()->attr('data-live-props-value'), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ final class InterceptChildComponentRenderSubscriberTest extends KernelTestCase
// in buildUrlForTodoListComponent
private static array $actualTodoItemFingerprints = [
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'0' => 'dSQ4+SgsF3QWeK4ngSOM1ROM50s6N1kWAK6bYW2JjZU=',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1' => 'sMvvf7q68tz/Cuk+vDeisDiq+7YPWzT+WZFzI37dGHY=',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'2' => '8AooEz36WYQyxj54BCaDm/jKbcdDdPDLaNO4/49bcQk=',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED.'0' => 'sMvvf7q68tz/Cuk+vDeisDiq+7YPWzT+WZFzI37dGHY=',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1' => '8AooEz36WYQyxj54BCaDm/jKbcdDdPDLaNO4/49bcQk=',
];

public function testItAllowsFullChildRenderOnMissingFingerprints(): void
Expand Down Expand Up @@ -74,6 +74,7 @@ public function testItRendersEmptyElementOnMatchingFingerprintWithCustomDataLive
public function testItRendersNewPropWhenFingerprintDoesNotMatch(): void
{
$fingerprints = self::$actualTodoItemFingerprints;
$fingerprints[AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED.'0'] = 'wrong fingerprint';
$fingerprints[AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX.'1'] = 'wrong fingerprint';

$this->browser()
Expand All @@ -93,11 +94,11 @@ public function testItRendersNewPropWhenFingerprintDoesNotMatch(): void
), $content);
// new props are JUST the "textLength" + a checksum for it specifically
$this->assertStringContainsString(sprintf(
'<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>',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
'<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>',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX_EMBEDDED
), $content);
$this->assertStringContainsString(sprintf(
'<li data-live-id="%s2"></li>',
'<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>',
AddLiveAttributesSubscriberTest::TODO_ITEM_DETERMINISTIC_PREFIX
), $content);
});
Expand All @@ -107,11 +108,13 @@ public function testItUsesKeysToRenderChildrenLiveIds(): void
{
$fingerprintValues = array_values(self::$actualTodoItemFingerprints);
$fingerprints = [];
$i = 0;
foreach ($fingerprintValues as $key => $fingerprintValue) {
$prefix = 0 !== $i++ %2 ? 'live-4172682817-the-key' : 'live-521026374-the-key';
// creating fingerprints keys to match todo_list_with_keys.html.twig
$fingerprints['live-1745423312-the-key'.$key] = $fingerprintValue;
$fingerprints[$prefix.$key] = $fingerprintValue;
}
$fingerprints['live-1745423312-the-key1'] = 'wrong fingerprint';
$fingerprints['live-4172682817-the-key1'] = 'wrong fingerprint';

$urlSimple = $this->doBuildUrlForComponent('todo_list_with_keys', []);
$urlWithChangedFingerprints = $this->doBuildUrlForComponent('todo_list_with_keys', $fingerprints);
Expand All @@ -122,12 +125,12 @@ public function testItUsesKeysToRenderChildrenLiveIds(): void
->assertHtml()
->assertElementCount('ul li', 3)
// check for the live-id we expect based on the key
->assertContains('data-live-id="live-1745423312-the-key0"')
->assertContains('data-live-id="live-521026374-the-key0"')
->assertNotContains('key="the-key0"')
->visit($urlWithChangedFingerprints)
->assertContains('<li data-live-id="live-1745423312-the-key0"></li>')
->assertContains('<li data-live-id="live-521026374-the-key0"></li>')
// this one is changed, so it renders a full element
->assertContains('<li data-live-name-value="todo_item" data-live-id="live-1745423312-the-key1"')
->assertContains('<li data-live-name-value="todo_item" data-live-id="live-4172682817-the-key1"')
;
}

Expand Down
17 changes: 12 additions & 5 deletions src/TwigComponent/src/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,21 @@ public function __construct(
) {
}

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

// allow the process to be short-circuited
if (null !== $rendered = $event->getRenderedString()) {
return $rendered;
return $event->getRenderedString();
}

public function createAndRender(string $name, array $props = []): string
{
if ($preRendered = $this->preCreateForRender($name, $props)) {
return $preRendered;
}

return $this->render($this->factory->create($name, $props));
Expand Down Expand Up @@ -77,7 +84,7 @@ public function render(MountedComponent $mounted): string
}

public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
{
{
$context[PreRenderEvent::EMBEDDED] = true;

$mounted = $this->factory->create($name, $props);
Expand Down
9 changes: 9 additions & 0 deletions src/TwigComponent/src/Twig/ComponentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ public function render(string $name, array $props = []): string
}
}

public function preRender(string $name, array $props): ?string
{
try {
return $this->container->get(ComponentRenderer::class)->preCreateForRender($name, $props);
} catch (\Throwable $e) {
$this->throwRuntimeError($name, $e);
}
}

public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
{
try {
Expand Down
35 changes: 33 additions & 2 deletions src/TwigComponent/src/Twig/ComponentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,37 @@ public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);

$compiler
->write('$preRendered = $this->extensions[')
->string(ComponentExtension::class)
->raw(']->preRender(')
->string($this->getAttribute('component'))
->raw(', ')
->raw('twig_to_array(')
->subcompile($this->getNode('variables'))
->raw(")")
->raw(");\n")
;

$compiler
->write('if (null !== $preRendered) {')
->raw("\n")
->indent()
->write('echo $preRendered;')
->raw("\n")
->outdent()
->write('} else {')
->raw("\n")
->indent()
;

$compiler
->write('$embeddedContext = $this->extensions[')
->string(ComponentExtension::class)
->raw(']->embeddedContext(')
->string($this->getAttribute('component'))
->raw(', twig_to_array(')
->raw(', ')
->raw('twig_to_array(')
->subcompile($this->getNode('variables'))
->raw('), ')
->raw($this->getAttribute('only') ? '[]' : '$context')
Expand All @@ -49,7 +74,7 @@ public function compile(Compiler $compiler): void
->raw($this->getAttribute('index'))
->raw(");\n")
;

$compiler->write('$embeddedBlocks = $embeddedContext[')
->string('outerBlocks')
->raw(']->convert($blocks, ')
Expand All @@ -60,5 +85,11 @@ public function compile(Compiler $compiler): void
$this->addGetTemplate($compiler);
$compiler->raw('->display($embeddedContext, $embeddedBlocks);');
$compiler->raw("\n");

$compiler
->outdent()
->write('}')
->raw("\n")
;
}
}

0 comments on commit 8f82680

Please sign in to comment.