Skip to content

Commit

Permalink
bug #1097 [LiveComponent] Only consider Live components in InterceptC…
Browse files Browse the repository at this point in the history
…hildComponentRenderSubscriber (sneakyvv)

This PR was merged into the 2.x branch.

Discussion
----------

[LiveComponent] Only consider Live components in InterceptChildComponentRenderSubscriber

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Tickets       |
| License       | MIT

This PR is related to the change of #1093.

### Problem
The component stack now also contains embedded Components until they are fully rendered, which is good. But, that also means that with this setup

```
{# someTemplate #}

<twig:aLiveComponent>
    <twig:aTwigComponent>
        <twig:anotherLiveComponent>
            {# ... #}
        </twig:anotherLiveComponent>
    </twig:aTwigComponent>
</twig:aLiveComponent>
```
when re-rendering the `anotherLiveComponent` the `InterceptChildComponentRenderSubscriber` would look at the parent component's `childrenFingerprints` to determine if it can short-circuit the rendering process for `anotherLiveComponent` and return an empty html element causing the FE to not do a callback to re-render it. However since that parent is a non-live component it will never short-circuit.

### Solution

Only take live components into account.

Commits
-------

953721e [LiveComponent] Only consider Live components in InterceptChildComponentRenderSubscriber
  • Loading branch information
weaverryan committed Sep 7, 2023
2 parents df14fce + 953721e commit 8d5a9f1
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Symfony\UX\LiveComponent\Twig\TemplateMap;
use Symfony\UX\LiveComponent\Util\ChildComponentPartialRenderer;
use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
use Symfony\UX\TwigComponent\ComponentFactory;
Expand Down Expand Up @@ -113,7 +114,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
$container->register('ux.live_component.event_listener.data_model_props_subscriber', DataModelPropsSubscriber::class)
->addTag('kernel.event_subscriber')
->setArguments([
new Reference('ux.twig_component.component_stack'),
new Reference('ux.twig_component.live_component_stack'),
new Reference('property_accessor'),
])
;
Expand All @@ -130,10 +131,16 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
$container->register('ux.live_component.live_responder', LiveResponder::class);
$container->setAlias(LiveResponder::class, 'ux.live_component.live_responder');

$container->register('ux.live_component.intercept_child_component_render_subscriber', InterceptChildComponentRenderSubscriber::class)
$container->register('ux.twig_component.live_component_stack', LiveComponentStack::class)
->setArguments([
new Reference('ux.twig_component.component_stack'),
])
;

$container->register('ux.live_component.intercept_child_component_render_subscriber', InterceptChildComponentRenderSubscriber::class)
->setArguments([
new Reference('ux.twig_component.live_component_stack'),
])
->addTag('container.service_subscriber', ['key' => DeterministicTwigIdCalculator::class, 'id' => 'ux.live_component.deterministic_id_calculator'])
->addTag('container.service_subscriber', ['key' => ChildComponentPartialRenderer::class, 'id' => 'ux.live_component.child_component_partial_renderer'])
->addTag('kernel.event_subscriber');
Expand Down
24 changes: 3 additions & 21 deletions src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\ModelBindingParser;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\Event\PreMountEvent;
use Symfony\UX\TwigComponent\MountedComponent;

/**
* Parses the "data-model" key, which triggers extra props to be passed in.
Expand All @@ -33,7 +31,7 @@ final class DataModelPropsSubscriber implements EventSubscriberInterface
{
private ModelBindingParser $modelBindingParser;

public function __construct(private ComponentStack $componentStack, private PropertyAccessorInterface $propertyAccessor)
public function __construct(private LiveComponentStack $componentStack, private PropertyAccessorInterface $propertyAccessor)
{
$this->modelBindingParser = new ModelBindingParser();
}
Expand All @@ -58,7 +56,7 @@ public function onPreMount(PreMountEvent $event): void

// find the first parent of the component about to be rendered that is a Live Component
// only those can have properties controlled via the data-model attribute
$parentMountedComponent = $this->getCurrentLiveComponent($this->componentStack);
$parentMountedComponent = $this->componentStack->getCurrentLiveComponent();
if (null === $parentMountedComponent) {
throw new \LogicException('You can only pass "data-model" when rendering a component when you\'re rendering inside of a parent component.');
}
Expand All @@ -79,20 +77,4 @@ public static function getSubscribedEvents(): array
PreMountEvent::class => 'onPreMount',
];
}

private function getCurrentLiveComponent(ComponentStack $componentStack): ?MountedComponent
{
foreach ($componentStack as $mountedComponent) {
if ($this->isLiveComponent($mountedComponent->getComponent()::class)) {
return $mountedComponent;
}
}

return null;
}

private function isLiveComponent(string $classname): bool
{
return [] !== (new \ReflectionClass($classname))->getAttributes(AsLiveComponent::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
use Symfony\UX\LiveComponent\Util\ChildComponentPartialRenderer;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;

/**
Expand All @@ -34,15 +34,15 @@ class InterceptChildComponentRenderSubscriber implements EventSubscriberInterfac
public const CHILDREN_FINGERPRINTS_METADATA_KEY = 'children_fingerprints';

public function __construct(
private ComponentStack $componentStack,
private LiveComponentStack $componentStack,
private ContainerInterface $container,
) {
}

public function preComponentCreated(PreCreateForRenderEvent $event): void
{
// if there is already a component, that's a parent. Else, this is not a child.
if (null === $parentComponent = $this->componentStack->getCurrentComponent()) {
if (null === $parentComponent = $this->componentStack->getCurrentLiveComponent()) {
return;
}

Expand Down
54 changes: 54 additions & 0 deletions src/LiveComponent/src/Util/LiveComponentStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Symfony\UX\LiveComponent\Util;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\MountedComponent;

/**
* This class decorates the TwigComponent\ComponentStack adding specific Live component functionalities.
*
* @author Bart Vanderstukken <bart.vanderstukken@gmail.com>
*
* @internal
*/
final class LiveComponentStack extends ComponentStack
{
public function __construct(private readonly ComponentStack $componentStack)
{
}

public function getCurrentLiveComponent(): ?MountedComponent
{
foreach ($this->componentStack as $mountedComponent) {
if ($this->isLiveComponent($mountedComponent->getComponent()::class)) {
return $mountedComponent;
}
}

return null;
}

private function isLiveComponent(string $classname): bool
{
return [] !== (new \ReflectionClass($classname))->getAttributes(AsLiveComponent::class);
}

public function getCurrentComponent(): ?MountedComponent
{
return $this->componentStack->getCurrentComponent();
}

public function getParentComponent(): ?MountedComponent
{
return $this->componentStack->getParentComponent();
}

public function hasParentComponent(): bool
{
return $this->componentStack->hasParentComponent();
}
}

0 comments on commit 8d5a9f1

Please sign in to comment.