Skip to content

Commit

Permalink
[React] Add permanent option to react_component function
Browse files Browse the repository at this point in the history
  • Loading branch information
smnandre authored and kbond committed Oct 21, 2024
1 parent c9bf5ee commit 4d60724
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 10 deletions.
5 changes: 5 additions & 0 deletions src/React/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`"
Expand Down
5 changes: 5 additions & 0 deletions src/React/assets/dist/render_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/React/assets/dist/render_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class default_1 extends Controller {
});
}
disconnect() {
if (this.permanentValue) {
return;
}
this.element.root.unmount();
this.dispatchEvent('unmount', {
component: this.componentValue,
Expand All @@ -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 };
10 changes: 8 additions & 2 deletions src/React/assets/src/render_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -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,
Expand Down
42 changes: 39 additions & 3 deletions src/React/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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:

Expand All @@ -82,6 +91,31 @@ For example:
<div {{ react_component('Admin/OtherComponent') }}></div>
{% 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 #}
<div {{ react_component('Hello', {fullName: 'Fabien'}, {permanent: true}) }}>
Loading...
</div>

.. _using-with-asset-mapper:

Using with AssetMapper
Expand Down Expand Up @@ -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/
17 changes: 13 additions & 4 deletions src/React/src/Twig/ReactComponentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,24 @@ public function getFunctions(): array
];
}

public function renderReactComponent(string $componentName, array $props = []): string
/**
* @param array<string, mixed> $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;
}
Expand Down
35 changes: 34 additions & 1 deletion src/React/tests/Twig/ReactComponentExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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="{&quot;fullName&quot;:&quot;Titouan Galopin&quot;}"', $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);
Expand Down

0 comments on commit 4d60724

Please sign in to comment.