vendor/symfony/ux-live-component/src/EventListener/InterceptChildComponentRenderSubscriber.php line 47

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\UX\LiveComponent\EventListener;
  11. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  12. use Symfony\UX\LiveComponent\LiveComponentHydrator;
  13. use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
  14. use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
  15. use Symfony\UX\LiveComponent\Util\JsonUtil;
  16. use Symfony\UX\LiveComponent\Util\TwigAttributeHelper;
  17. use Symfony\UX\TwigComponent\ComponentFactory;
  18. use Symfony\UX\TwigComponent\ComponentStack;
  19. use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
  20. /**
  21.  * Responsible for rendering children as empty elements during a re-render.
  22.  *
  23.  * @author Ryan Weaver <ryan@symfonycasts.com>
  24.  *
  25.  * @experimental
  26.  *
  27.  * @internal
  28.  */
  29. class InterceptChildComponentRenderSubscriber implements EventSubscriberInterface
  30. {
  31.     public const CHILDREN_FINGERPRINTS_METADATA_KEY 'children_fingerprints';
  32.     public function __construct(
  33.         private ComponentStack $componentStack,
  34.         private DeterministicTwigIdCalculator $deterministicTwigIdCalculator,
  35.         private FingerprintCalculator $fingerprintCalculator,
  36.         private TwigAttributeHelper $twigAttributeHelper,
  37.         private ComponentFactory $componentFactory,
  38.         private LiveComponentHydrator $liveComponentHydrator,
  39.     ) {
  40.     }
  41.     public function preComponentCreated(PreCreateForRenderEvent $event): void
  42.     {
  43.         // if there is already a component, that's a parent. Else, this is not a child.
  44.         if (!$this->componentStack->getCurrentComponent()) {
  45.             return;
  46.         }
  47.         $parentComponent $this->componentStack->getCurrentComponent();
  48.         if (!$parentComponent->hasExtraMetadata(self::CHILDREN_FINGERPRINTS_METADATA_KEY)) {
  49.             return;
  50.         }
  51.         $childFingerprints $parentComponent->getExtraMetadata(self::CHILDREN_FINGERPRINTS_METADATA_KEY);
  52.         // get the deterministic id for this child, but without incrementing the counter yet
  53.         $deterministicId $this->deterministicTwigIdCalculator->calculateDeterministicId(incrementfalse);
  54.         if (!isset($childFingerprints[$deterministicId])) {
  55.             // child fingerprint wasn't set, it is likely a new child, allow it to render fully
  56.             return;
  57.         }
  58.         // increment the internal counter now to keep "counter" consistency if we're
  59.         // in a loop of children being rendered on the same line
  60.         // we need to do this because this component will *not* ever hit
  61.         // AddLiveAttributesSubscriber where the counter is normally incremented
  62.         $this->deterministicTwigIdCalculator->calculateDeterministicId(incrementtrue);
  63.         $newPropsFingerprint $this->fingerprintCalculator->calculateFingerprint($event->getProps());
  64.         if ($childFingerprints[$deterministicId] === $newPropsFingerprint) {
  65.             // the props passed to create this child have *not* changed
  66.             // return an empty element so the frontend knows to keep the current child
  67.             $rendered sprintf(
  68.                 '<div data-live-id="%s"></div>',
  69.                 $this->twigAttributeHelper->escapeAttribute($deterministicId)
  70.             );
  71.             $event->setRenderedString($rendered);
  72.             return;
  73.         }
  74.         /*
  75.          * The props passed to create this child HAVE changed.
  76.          * Send back a fake element with:
  77.          *      * data-live-id
  78.          *      * data-live-fingerprint-value (new fingerprint)
  79.          *      * data-live-props-value (new dehydrated props)
  80.          */
  81.         $mounted $this->componentFactory->create($event->getName(), $event->getProps());
  82.         $dehydratedComponent $this->liveComponentHydrator->dehydrate($mounted);
  83.         $rendered sprintf(
  84.             '<div data-live-id="%s" data-live-fingerprint-value="%s" data-live-props-value="%s"></div>',
  85.             $this->twigAttributeHelper->escapeAttribute($deterministicId),
  86.             $this->twigAttributeHelper->escapeAttribute($newPropsFingerprint),
  87.             $this->twigAttributeHelper->escapeAttribute(JsonUtil::encodeObject($dehydratedComponent->getProps()))
  88.         );
  89.         $event->setRenderedString($rendered);
  90.     }
  91.     public static function getSubscribedEvents(): array
  92.     {
  93.         return [
  94.             PreCreateForRenderEvent::class => 'preComponentCreated',
  95.         ];
  96.     }
  97. }