From d638782a31f76fc27d1f397b657e07a51d96e1ef Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 13 Jan 2022 19:03:48 -0500 Subject: [PATCH] [Twig][Live] add html attributes system - adds `HasAttributesTrait` for components which makes `attributes` variable available in component templates - adds `PostMount` component hook to intercept extra props - adds `PreRenderEvent` to intercept/manipulate twig template/variables before rendering - [BC BREAK] Remove `init_live_component()` twig function, use `{{ attributes }}` instead. --- src/LiveComponent/CHANGELOG.md | 6 + src/LiveComponent/composer.json | 2 +- .../LiveComponentExtension.php | 8 + .../AddLiveAttributesSubscriber.php | 56 ++++++ .../EventListener/LiveComponentSubscriber.php | 23 ++- src/LiveComponent/src/Resources/doc/index.rst | 77 ++++++-- .../src/Twig/LiveComponentExtension.php | 1 - .../src/Twig/LiveComponentRuntime.php | 59 +++--- .../Component/ComponentWithAttributes.php | 17 ++ src/LiveComponent/tests/Fixture/Kernel.php | 2 + .../templates/components/component1.html.twig | 4 +- .../templates/components/component2.html.twig | 4 +- .../templates/components/component6.html.twig | 4 +- .../AddLiveAttributesSubscriberTest.php} | 4 +- .../Integration/LiveComponentHydratorTest.php | 49 +++++ src/TwigComponent/CHANGELOG.md | 7 + src/TwigComponent/composer.json | 1 + .../src/Attribute/AsTwigComponent.php | 11 ++ src/TwigComponent/src/Attribute/PostMount.php | 32 ++++ src/TwigComponent/src/ComponentAttributes.php | 89 +++++++++ src/TwigComponent/src/ComponentFactory.php | 58 +++--- src/TwigComponent/src/ComponentMetadata.php | 61 ++++++ src/TwigComponent/src/ComponentRenderer.php | 27 ++- .../Compiler/TwigComponentPass.php | 1 - .../TwigComponentExtension.php | 1 + .../src/EventListener/PreRenderEvent.php | 82 ++++++++ src/TwigComponent/src/HasAttributesTrait.php | 80 ++++++++ src/TwigComponent/src/Resources/doc/index.rst | 177 ++++++++++++++++++ .../src/Twig/ComponentRuntime.php | 2 +- .../tests/Fixture/Component/ComponentB.php | 10 + .../Fixture/Component/WithAttributes.php | 23 +++ src/TwigComponent/tests/Fixture/Kernel.php | 2 + .../templates/components/custom1.html.twig | 1 + .../components/with_attributes.html.twig | 1 + .../Fixture/templates/template_a.html.twig | 2 + .../Fixture/templates/template_b.html.twig | 2 +- .../Integration/ComponentExtensionTest.php | 11 ++ .../Integration/ComponentFactoryTest.php | 87 ++------- .../Unit/Attribute/AsTwigComponentTest.php | 28 +++ .../tests/Unit/ComponentAttributesTest.php | 58 ++++++ 40 files changed, 980 insertions(+), 190 deletions(-) create mode 100644 src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php create mode 100644 src/LiveComponent/tests/Fixture/Component/ComponentWithAttributes.php rename src/LiveComponent/tests/Functional/{Twig/LiveComponentExtensionTest.php => EventListener/AddLiveAttributesSubscriberTest.php} (89%) create mode 100644 src/TwigComponent/src/Attribute/PostMount.php create mode 100644 src/TwigComponent/src/ComponentAttributes.php create mode 100644 src/TwigComponent/src/ComponentMetadata.php create mode 100644 src/TwigComponent/src/EventListener/PreRenderEvent.php create mode 100644 src/TwigComponent/src/HasAttributesTrait.php create mode 100644 src/TwigComponent/tests/Fixture/Component/WithAttributes.php create mode 100644 src/TwigComponent/tests/Fixture/templates/components/with_attributes.html.twig create mode 100644 src/TwigComponent/tests/Unit/ComponentAttributesTest.php diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md index 824a8167d96..cb13a102727 100644 --- a/src/LiveComponent/CHANGELOG.md +++ b/src/LiveComponent/CHANGELOG.md @@ -7,6 +7,12 @@ - Send live action arguments to backend +- [BC BREAK] Remove `init_live_component()` twig function, use `{{ attributes }}` instead: + ```diff + -
+ +
+ ``` + ## 2.0.0 - Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus` diff --git a/src/LiveComponent/composer.json b/src/LiveComponent/composer.json index 90f578db7f6..21ff0e06f7e 100644 --- a/src/LiveComponent/composer.json +++ b/src/LiveComponent/composer.json @@ -27,7 +27,7 @@ }, "require": { "php": ">=8.0", - "symfony/ux-twig-component": "^2.0" + "symfony/ux-twig-component": "^2.1" }, "require-dev": { "doctrine/annotations": "^1.0", diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php index c782afe7e30..1f8ed8c8bd9 100644 --- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php +++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php @@ -21,6 +21,7 @@ use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\ComponentValidator; use Symfony\UX\LiveComponent\ComponentValidatorInterface; +use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber; use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber; use Symfony\UX\LiveComponent\LiveComponentHydrator; use Symfony\UX\LiveComponent\PropertyHydratorInterface; @@ -46,6 +47,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { 'key' => $attribute->name, 'template' => $attribute->template, 'default_action' => $attribute->defaultAction, + 'live' => true, ])) ->addTag('controller.service_arguments') ; @@ -78,6 +80,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { $container->register('ux.live_component.twig.component_runtime', LiveComponentRuntime::class) ->setArguments([ + new Reference('twig'), new Reference('ux.live_component.component_hydrator'), new Reference('ux.twig_component.component_factory'), new Reference(UrlGeneratorInterface::class), @@ -90,6 +93,11 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { ->addTag('container.service_subscriber', ['key' => 'validator', 'id' => 'validator']) ; + $container->register('ux.live_component.add_attributes_subscriber', AddLiveAttributesSubscriber::class) + ->addTag('kernel.event_subscriber') + ->addTag('container.service_subscriber', ['key' => LiveComponentRuntime::class, 'id' => 'ux.live_component.twig.component_runtime']) + ; + $container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class); } } diff --git a/src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php b/src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php new file mode 100644 index 00000000000..ab7297623cc --- /dev/null +++ b/src/LiveComponent/src/EventListener/AddLiveAttributesSubscriber.php @@ -0,0 +1,56 @@ + + */ +final class AddLiveAttributesSubscriber implements EventSubscriberInterface, ServiceSubscriberInterface +{ + public function __construct(private ContainerInterface $container) + { + } + + public function onPreRender(PreRenderEvent $event): void + { + if (!$event->getMetadata()->get('live', false)) { + // not a live component, skip + return; + } + + /** @var ComponentAttributes $attributes */ + $attributes = $this->container->get(LiveComponentRuntime::class) + ->getLiveAttributes($event->getComponent(), $event->getMetadata()) + ; + + $variables = $event->getVariables(); + + if (isset($variables['attributes']) && $variables['attributes'] instanceof ComponentAttributes) { + // merge with existing attributes if available + $attributes = $attributes->defaults($variables['attributes']->all()); + } + + $variables['attributes'] = $attributes; + + $event->setVariables($variables); + } + + public static function getSubscribedEvents(): array + { + return [PreRenderEvent::class => 'onPreRender']; + } + + public static function getSubscribedServices(): array + { + return [ + LiveComponentRuntime::class, + ]; + } +} diff --git a/src/LiveComponent/src/EventListener/LiveComponentSubscriber.php b/src/LiveComponent/src/EventListener/LiveComponentSubscriber.php index 06569b083fc..dd54f30f051 100644 --- a/src/LiveComponent/src/EventListener/LiveComponentSubscriber.php +++ b/src/LiveComponent/src/EventListener/LiveComponentSubscriber.php @@ -31,6 +31,7 @@ use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\LiveComponentHydrator; use Symfony\UX\TwigComponent\ComponentFactory; +use Symfony\UX\TwigComponent\ComponentMetadata; use Symfony\UX\TwigComponent\ComponentRenderer; /** @@ -73,24 +74,28 @@ public function onKernelRequest(RequestEvent $event): void $componentName = (string) $request->get('component'); try { - $config = $this->container->get(ComponentFactory::class)->configFor($componentName); + /** @var ComponentMetadata $metadata */ + $metadata = $this->container->get(ComponentFactory::class)->metadataFor($componentName); } catch (\InvalidArgumentException $e) { throw new NotFoundHttpException(sprintf('Component "%s" not found.', $componentName), $e); } - $request->attributes->set('_component_template', $config['template']); + if (!$metadata->get('live', false)) { + throw new NotFoundHttpException(sprintf('"%s" (%s) is not a Live Component.', $metadata->getClass(), $componentName)); + } + + $request->attributes->set('_component_metadata', $metadata); if ('get' === $action) { - $defaultAction = trim($config['default_action'] ?? '__invoke', '()'); - $componentClass = $config['class']; + $defaultAction = trim($metadata->get('default_action', '__invoke'), '()'); - if (!method_exists($componentClass, $defaultAction)) { + if (!method_exists($metadata->getClass(), $defaultAction)) { // todo should this check be in a compiler pass to ensure fails at compile time? - throw new \LogicException(sprintf('Live component "%s" requires the default action method "%s".%s', $componentClass, $defaultAction, '__invoke' === $defaultAction ? ' Either add this method or use the DefaultActionTrait' : '')); + throw new \LogicException(sprintf('Live component "%s" (%s) requires the default action method "%s".%s', $metadata->getClass(), $componentName, $defaultAction, '__invoke' === $defaultAction ? ' Either add this method or use the DefaultActionTrait' : '')); } // set default controller for "default" action - $request->attributes->set('_controller', sprintf('%s::%s', $config['service_id'], $defaultAction)); + $request->attributes->set('_controller', sprintf('%s::%s', $metadata->getServiceId(), $defaultAction)); $request->attributes->set('_component_default_action', true); return; @@ -106,7 +111,7 @@ public function onKernelRequest(RequestEvent $event): void throw new BadRequestHttpException('Invalid CSRF token.'); } - $request->attributes->set('_controller', sprintf('%s::%s', $config['service_id'], $action)); + $request->attributes->set('_controller', sprintf('%s::%s', $metadata->getServiceId(), $action)); } public function onKernelController(ControllerEvent $event): void @@ -232,7 +237,7 @@ private function createResponse(object $component, Request $request): Response $html = $this->container->get(ComponentRenderer::class)->render( $component, - $request->attributes->get('_component_template') + $request->attributes->get('_component_metadata') ); return new Response($html); diff --git a/src/LiveComponent/src/Resources/doc/index.rst b/src/LiveComponent/src/Resources/doc/index.rst index 9ad6ac1748a..41891718cc3 100644 --- a/src/LiveComponent/src/Resources/doc/index.rst +++ b/src/LiveComponent/src/Resources/doc/index.rst @@ -43,10 +43,15 @@ A real-time product search component might look like this:: The ability to reference local variables in the template (e.g. ``query``) was added in TwigComponents 2.1. Previously, all data needed to be referenced through ``this`` (e.g. ``this.query``). +.. versionadded:: 2.1 + + The ability to initialize live component with the ``attributes`` twig variable was added in LiveComponents + 2.1. Previously, the ``init_live_component()`` function was required (this function was removed in 2.1). + .. code-block:: twig {# templates/components/product_search.html.twig #} -
+
- +
+ +
{{ this.randomNumber }}
@@ -176,7 +181,7 @@ and give the user a new random number: .. code-block:: twig -
+
{{ this.randomNumber }}