diff --git a/src/React/CHANGELOG.md b/src/React/CHANGELOG.md index 77b48ddca31..c9c5c066314 100644 --- a/src/React/CHANGELOG.md +++ b/src/React/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 2.21.0 + +- Add `permanent` option to the `react_component` Twig function, to prevent the + _unmounting_ when the component is deconnected and immediately re-connected. + ## 2.13.2 - Revert "Change JavaScript package to `type: module`" diff --git a/src/React/assets/dist/render_controller.d.ts b/src/React/assets/dist/render_controller.d.ts index fc7b1dd4371..8f6dbd18037 100644 --- a/src/React/assets/dist/render_controller.d.ts +++ b/src/React/assets/dist/render_controller.d.ts @@ -3,9 +3,14 @@ import { Controller } from '@hotwired/stimulus'; export default class extends Controller { readonly componentValue?: string; readonly propsValue?: object; + readonly permanentValue: boolean; static values: { component: StringConstructor; props: ObjectConstructor; + permanent: { + type: BooleanConstructor; + default: boolean; + }; }; connect(): void; disconnect(): void; diff --git a/src/React/assets/dist/render_controller.js b/src/React/assets/dist/render_controller.js index 5b8f9fb8e7c..c66a32044ed 100644 --- a/src/React/assets/dist/render_controller.js +++ b/src/React/assets/dist/render_controller.js @@ -54,6 +54,9 @@ class default_1 extends Controller { }); } disconnect() { + if (this.permanentValue) { + return; + } this.element.root.unmount(); this.dispatchEvent('unmount', { component: this.componentValue, @@ -74,6 +77,7 @@ class default_1 extends Controller { default_1.values = { component: String, props: Object, + permanent: { type: Boolean, default: false }, }; export { default_1 as default }; diff --git a/src/React/assets/src/render_controller.ts b/src/React/assets/src/render_controller.ts index 595b5a6ae22..b35d049956b 100644 --- a/src/React/assets/src/render_controller.ts +++ b/src/React/assets/src/render_controller.ts @@ -14,17 +14,17 @@ import { Controller } from '@hotwired/stimulus'; export default class extends Controller { declare readonly componentValue?: string; declare readonly propsValue?: object; + declare readonly permanentValue: boolean; static values = { component: String, props: Object, + permanent: { type: Boolean, default: false }, }; connect() { const props = this.propsValue ? this.propsValue : null; - this.dispatchEvent('connect', { component: this.componentValue, props: props }); - if (!this.componentValue) { throw new Error('No component specified.'); } @@ -40,6 +40,12 @@ export default class extends Controller { } disconnect() { + if (this.permanentValue) { + // Prevent unmounting the component if the controller is permanent + // (no render is allowed after unmounting) + return; + } + (this.element as any).root.unmount(); this.dispatchEvent('unmount', { component: this.componentValue, diff --git a/src/React/doc/index.rst b/src/React/doc/index.rst index 06a2b2854fe..96e16e0aa1e 100644 --- a/src/React/doc/index.rst +++ b/src/React/doc/index.rst @@ -2,12 +2,14 @@ Symfony UX React ================ Symfony UX React is a Symfony bundle integrating `React`_ in -Symfony applications. It is part of `the Symfony UX initiative`_. +Symfony applications. It is part of the `Symfony UX initiative`_. React is a JavaScript library for building user interfaces. Symfony UX React provides tools to render React components from Twig, handling rendering and data transfers. +You can see a live example of this integration on the `Symfony UX React demo`_. + Symfony UX React supports React 18+. Installation @@ -41,6 +43,9 @@ React components. Usage ----- +Register components +~~~~~~~~~~~~~~~~~~~ + The Flex recipe will have already added the ``registerReactControllerComponents()`` code to your ``assets/app.js`` file: @@ -55,7 +60,11 @@ This will load all React components located in the ``assets/react/controllers`` directory. These are known as **React controller components**: top-level components that are meant to be rendered from Twig. -You can render any React controller component in Twig using the ``react_component()``. +Render in Twig +~~~~~~~~~~~~~~ + +You can render any React controller component in your Twig templates, using the +``react_component()`` function. For example: @@ -82,6 +91,31 @@ For example:
{% endblock %} +Permanent components +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.21 + + The ability to mark a component ``permanent`` was added in UX React 2.21. + +The controller responsible to render the React components can be configured +to keep the React component mounted when the root element is removed from +the DOM, using the ``permanent`` option. + +This is particularly useful when the root element of a component is moved around +in the DOM or is removed and immediately re-added to the DOM (e.g. when using +`Turbo`_ and its `data-turbo-permanent` attribute). + +.. code-block:: html+twig + + {# templates/home.html.twig #} + {% extends 'base.html.twig' %} + + {# The React component will stay mounted if the div is moved in the DOM #} +
+ Loading... +
+ .. _using-with-asset-mapper: Using with AssetMapper @@ -119,4 +153,6 @@ the Symfony framework: https://symfony.com/doc/current/contributing/code/bc.html .. _`React`: https://reactjs.org/ -.. _`the Symfony UX initiative`: https://ux.symfony.com/ +.. _`Symfony UX initiative`: https://ux.symfony.com/ +.. _`Symfony UX React demo`: https://ux.symfony.com/react +:: _`Turbo`: https://turbo.hotwire.dev/ diff --git a/src/React/src/Twig/ReactComponentExtension.php b/src/React/src/Twig/ReactComponentExtension.php index aabbbd9e12a..9780df3147b 100644 --- a/src/React/src/Twig/ReactComponentExtension.php +++ b/src/React/src/Twig/ReactComponentExtension.php @@ -45,15 +45,24 @@ public function getFunctions(): array ]; } - public function renderReactComponent(string $componentName, array $props = []): string + /** + * @param array $props + * @param array{permanent?: bool} $options + */ + public function renderReactComponent(string $componentName, array $props = [], array $options = []): string { - $params = ['component' => $componentName]; + $values = ['component' => $componentName]; if ($props) { - $params['props'] = $props; + $values['props'] = $props; + } + if ($options) { + if (\is_bool($permanent = $options['permanent'] ?? null)) { + $values['permanent'] = $permanent; + } } $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes(); - $stimulusAttributes->addController('@symfony/ux-react/react', $params); + $stimulusAttributes->addController('@symfony/ux-react/react', $values); return (string) $stimulusAttributes; } diff --git a/src/React/tests/Twig/ReactComponentExtensionTest.php b/src/React/tests/Twig/ReactComponentExtensionTest.php index 9fbf8c8b0a3..a522572f16b 100644 --- a/src/React/tests/Twig/ReactComponentExtensionTest.php +++ b/src/React/tests/Twig/ReactComponentExtensionTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\UX\React\Tests; +namespace Symfony\UX\React\Tests\Twig; use PHPUnit\Framework\TestCase; use Symfony\UX\React\Tests\Kernel\TwigAppKernel; @@ -41,6 +41,39 @@ public function testRenderComponent() ); } + /** + * @dataProvider provideOptions + */ + public function testRenderComponentWithOptions(array $options, string|false $expected) + { + $kernel = new TwigAppKernel('test', true); + $kernel->boot(); + + /** @var ReactComponentExtension $extension */ + $extension = $kernel->getContainer()->get('test.twig.extension.react'); + + $rendered = $extension->renderReactComponent( + 'SubDir/MyComponent', + ['fullName' => 'Titouan Galopin'], + $options, + ); + + $this->assertStringContainsString('data-controller="symfony--ux-react--react" data-symfony--ux-react--react-component-value="SubDir/MyComponent" data-symfony--ux-react--react-props-value="{"fullName":"Titouan Galopin"}"', $rendered); + if (false === $expected) { + $this->assertStringNotContainsString('data-symfony--ux-react--react-permanent-value', $rendered); + } else { + $this->assertStringContainsString($expected, $rendered); + } + } + + public static function provideOptions(): iterable + { + yield 'permanent' => [['permanent' => true], 'data-symfony--ux-react--react-permanent-value="true"']; + yield 'not permanent' => [['permanent' => false], 'data-symfony--ux-react--react-permanent-value="false"']; + yield 'permanent not bool' => [['permanent' => 12345], false]; + yield 'no permanent' => [[], false]; + } + public function testRenderComponentWithoutProps() { $kernel = new TwigAppKernel('test', true);