From 23d4b7c9b124a69e7f046124503fefaddd10df24 Mon Sep 17 00:00:00 2001 From: Nicolas Rigaud Date: Wed, 8 Nov 2023 11:22:02 +0100 Subject: [PATCH] Allow co living components with discriminator attribute --- .../assets/dist/live_controller.js | 4 +-- .../Component/plugins/QueryStringPlugin.ts | 2 +- src/LiveComponent/assets/src/url_utils.ts | 2 +- .../QueryStringInitializeSubscriber.php | 7 +++- .../src/Metadata/LivePropMetadata.php | 2 +- .../Util/LiveControllerAttributesCreator.php | 35 +++++++++++++------ .../src/Util/QueryStringPropsExtractor.php | 4 +-- ..._components_with_url_bound_props.html.twig | 2 ++ .../AddLiveAttributesSubscriberTest.php | 31 ++++++++++++++++ 9 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 src/LiveComponent/tests/Fixtures/templates/render_multiple_components_with_url_bound_props.html.twig diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index 8986e513a23..925c1e36f0a 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -2734,7 +2734,7 @@ function fromQueryString(search) { const insertDotNotatedValueIntoData = (key, value, data) => { const [first, second, ...rest] = key.split('.'); if (!second) - return data[key] = value; + return (data[key] = value); if (data[first] === undefined) { data[first] = Number.isNaN(Number.parseInt(second)) ? {} : []; } @@ -2819,7 +2819,7 @@ class QueryStringPlugin { if (typeof value !== 'object') { return false; } - for (let key of Object.keys(value)) { + for (const key of Object.keys(value)) { if (!this.isEmpty(value[key])) { return false; } diff --git a/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts index 65f2cc9edcb..dc464858601 100644 --- a/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts @@ -43,7 +43,7 @@ export default class implements PluginInterface { return false; } - for (let key of Object.keys(value)) { + for (const key of Object.keys(value)) { if (!this.isEmpty(value[key])) { return false; } diff --git a/src/LiveComponent/assets/src/url_utils.ts b/src/LiveComponent/assets/src/url_utils.ts index a233b407142..10a4ae99718 100644 --- a/src/LiveComponent/assets/src/url_utils.ts +++ b/src/LiveComponent/assets/src/url_utils.ts @@ -54,7 +54,7 @@ function fromQueryString(search: string) { const [first, second, ...rest] = key.split('.'); // We're at a leaf node, let's make the assigment... - if (!second) return data[key] = value; + if (!second) return (data[key] = value); // This is where we fill in empty arrays/objects along the way to the assigment... if (data[first] === undefined) { diff --git a/src/LiveComponent/src/EventListener/QueryStringInitializeSubscriber.php b/src/LiveComponent/src/EventListener/QueryStringInitializeSubscriber.php index bc957ccc909..6c8d57df5bb 100644 --- a/src/LiveComponent/src/EventListener/QueryStringInitializeSubscriber.php +++ b/src/LiveComponent/src/EventListener/QueryStringInitializeSubscriber.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; +use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator; use Symfony\UX\LiveComponent\Util\QueryStringPropsExtractor; use Symfony\UX\TwigComponent\Event\PreMountEvent; @@ -58,7 +59,11 @@ public function onPreMount(PreMountEvent $event): void $request = $this->requestStack->getMainRequest(); - $queryStringData = $this->queryStringPropsExtractor->extract($request, $metadata, $component); + $prefix = $data[LiveControllerAttributesCreator::URL_PREFIX_PROP_NAME] + ?? $data[LiveControllerAttributesCreator::KEY_PROP_NAME] + ?? ''; + + $queryStringData = $this->queryStringPropsExtractor->extract($request, $metadata, $component, $prefix); $event->setData(array_merge($data, $queryStringData)); } diff --git a/src/LiveComponent/src/Metadata/LivePropMetadata.php b/src/LiveComponent/src/Metadata/LivePropMetadata.php index ce16992063f..52356c72d22 100644 --- a/src/LiveComponent/src/Metadata/LivePropMetadata.php +++ b/src/LiveComponent/src/Metadata/LivePropMetadata.php @@ -55,7 +55,7 @@ public function allowsNull(): bool } /** - * @return array{'parameters': array} + * @return array{'name': string} */ public function getQueryStringMapping(): array { diff --git a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php index 0a13349de3c..a7db4be921c 100644 --- a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php +++ b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php @@ -39,6 +39,8 @@ class LiveControllerAttributesCreator */ public const KEY_PROP_NAME = 'key'; + public const URL_PREFIX_PROP_NAME = 'query-param-prefix'; + public function __construct( private LiveComponentMetadataFactory $metadataFactory, private LiveComponentHydrator $hydrator, @@ -108,6 +110,28 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad } } + $liveMetadata = $this->metadataFactory->getMetadata($mounted->getName()); + + if ($liveMetadata->hasQueryStringBindings()) { + $urlPrefix = $mountedAttributes->all()[self::URL_PREFIX_PROP_NAME] + ?? $mounted->getInputProps()[self::KEY_PROP_NAME] + ?? ''; + + $queryMapping = []; + foreach ($liveMetadata->getAllLivePropsMetadata() as $livePropMetadata) { + if ($mapping = $livePropMetadata->getQueryStringMapping()) { + $mapping['name'] = $urlPrefix.$mapping['name']; + $queryMapping[$livePropMetadata->getName()] = $mapping; + } + } + $attributesCollection->setQueryUrlMapping($queryMapping); + + if ('' !== $urlPrefix) { + // So the prefix is also used when the component is re-rendered in live + $mountedAttributes = $mountedAttributes->defaults([self::URL_PREFIX_PROP_NAME => $urlPrefix]); + } + } + $dehydratedProps = $this->dehydrateComponent( $mounted->getName(), $mounted->getComponent(), @@ -121,17 +145,6 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad ); } - $liveMetadata = $this->metadataFactory->getMetadata($mounted->getName()); - if ($liveMetadata->hasQueryStringBindings()) { - $queryMapping = []; - foreach ($liveMetadata->getAllLivePropsMetadata() as $livePropMetadata) { - if ($mapping = $livePropMetadata->getQueryStringMapping()) { - $queryMapping[$livePropMetadata->getName()] = $mapping; - } - } - $attributesCollection->setQueryUrlMapping($queryMapping); - } - return $attributesCollection; } diff --git a/src/LiveComponent/src/Util/QueryStringPropsExtractor.php b/src/LiveComponent/src/Util/QueryStringPropsExtractor.php index 74fe010aa82..14b1c7dc86b 100644 --- a/src/LiveComponent/src/Util/QueryStringPropsExtractor.php +++ b/src/LiveComponent/src/Util/QueryStringPropsExtractor.php @@ -28,7 +28,7 @@ public function __construct(private readonly LiveComponentHydrator $hydrator) { } - public function extract(Request $request, LiveComponentMetadata $metadata, object $component): array + public function extract(Request $request, LiveComponentMetadata $metadata, object $component, string $prefix = ''): array { $query = $request->query->all(); @@ -39,7 +39,7 @@ public function extract(Request $request, LiveComponentMetadata $metadata, objec foreach ($metadata->getAllLivePropsMetadata() as $livePropMetadata) { if ($queryStringMapping = $livePropMetadata->getQueryStringMapping()) { - if (null !== ($value = $query[$queryStringMapping['name']] ?? null)) { + if (null !== ($value = $query[$prefix.$queryStringMapping['name']] ?? null)) { if (\is_array($value) && $this->isNumericIndexedArray($value)) { ksort($value); } diff --git a/src/LiveComponent/tests/Fixtures/templates/render_multiple_components_with_url_bound_props.html.twig b/src/LiveComponent/tests/Fixtures/templates/render_multiple_components_with_url_bound_props.html.twig new file mode 100644 index 00000000000..79922b7585e --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/templates/render_multiple_components_with_url_bound_props.html.twig @@ -0,0 +1,2 @@ +{{ component('component_with_url_bound_props', {'id': 'component1', 'query-param-prefix': 'c1_'}) }} +{{ component('component_with_url_bound_props', {'id': 'component2', 'key': 'c2_'}) }} diff --git a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php index 1ef2c295146..75b91331a2a 100644 --- a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php @@ -153,4 +153,35 @@ public function testQueryStringMappingAttribute() $this->assertEquals($expected, $queryMapping); } + + public function testQueryStringMappingAttributeWithMultipleComponents() + { + $crawler = $this->browser() + ->visit('/render-template/render_multiple_components_with_url_bound_props') + ->assertSuccessful() + ->crawler() + ; + $component1 = $crawler->filter('div[id=component1]'); + $this->assertEquals('c1_', $component1->attr('query-param-prefix')); + $queryMapping = json_decode($component1->attr('data-live-query-mapping-value'), true); + $expected = [ + 'prop1' => ['name' => 'c1_prop1'], + 'prop2' => ['name' => 'c1_prop2'], + 'prop3' => ['name' => 'c1_prop3'], + 'prop5' => ['name' => 'c1_prop5'], + ]; + + $this->assertEquals($expected, $queryMapping); + + $component2 = $crawler->filter('div[id=component2]'); + $this->assertNull($component2->attr('query-param-prefix')); + $queryMapping = json_decode($component2->attr('data-live-query-mapping-value'), true); + $expected = [ + 'prop1' => ['name' => 'c2_prop1'], + 'prop2' => ['name' => 'c2_prop2'], + 'prop3' => ['name' => 'c2_prop3'], + 'prop5' => ['name' => 'c2_prop5'], + ]; + $this->assertEquals($expected, $queryMapping); + } }