Skip to content

Commit 6b5a0df

Browse files
tgalopinweaverryan
authored andcommitted
Introduce UX React component
1 parent 040e75d commit 6b5a0df

34 files changed

+890
-8
lines changed

.github/workflows/test.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ jobs:
101101
run: php vendor/bin/simple-phpunit
102102
working-directory: src/LazyImage
103103

104+
- name: React Dependencies
105+
uses: ramsey/composer-install@v2
106+
with:
107+
working-directory: src/React
108+
dependency-versions: lowest
109+
- name: React Tests
110+
run: php vendor/bin/simple-phpunit
111+
working-directory: src/React
112+
104113
tests-php8-low-deps:
105114
runs-on: ubuntu-latest
106115
steps:
@@ -184,6 +193,14 @@ jobs:
184193
working-directory: src/LiveComponent
185194
run: php vendor/bin/simple-phpunit
186195

196+
- name: React Dependencies
197+
uses: ramsey/composer-install@v2
198+
with:
199+
working-directory: src/React
200+
- name: React Tests
201+
working-directory: src/React
202+
run: php vendor/bin/simple-phpunit
203+
187204
tests-php81-high-deps:
188205
runs-on: ubuntu-latest
189206
steps:

babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
presets: [
33
['@babel/preset-env', {targets: {node: 'current'}}],
4+
'@babel/react',
45
'@babel/preset-typescript',
56
],
67
};

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ module.exports = {
77
path.join(__dirname, 'test/setup.js'),
88
],
99
transform: {
10-
'\\.(j|t)s$': ['babel-jest', { configFile: path.join(__dirname, './babel.config.js') }]
10+
'\\.(j|t)s': ['babel-jest', { configFile: path.join(__dirname, './babel.config.js') }]
1111
},
1212
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"devDependencies": {
1616
"@babel/core": "^7.15.8",
1717
"@babel/preset-env": "^7.15.8",
18-
"@babel/preset-typescript": "^7.15.0",
18+
"@babel/preset-react": "^7.15.8",
19+
"@babel/preset-typescript": "^7.15.8",
1920
"@rollup/plugin-node-resolve": "^13.0.0",
2021
"@rollup/plugin-typescript": "^8.3.0",
2122
"@symfony/stimulus-testing": "^2.0.1",

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -971,11 +971,14 @@ function haveRenderedValuesChanged(originalDataJson, currentDataJson, newDataJso
971971
}
972972

973973
function normalizeAttributesForComparison(element) {
974-
if (element.value) {
975-
element.setAttribute('value', element.value);
976-
}
977-
else if (element.hasAttribute('value')) {
978-
element.setAttribute('value', '');
974+
const isFileInput = element instanceof HTMLInputElement && element.type === 'file';
975+
if (!isFileInput) {
976+
if (element.value) {
977+
element.setAttribute('value', element.value);
978+
}
979+
else if (element.hasAttribute('value')) {
980+
element.setAttribute('value', '');
981+
}
979982
}
980983
Array.from(element.children).forEach((child) => {
981984
normalizeAttributesForComparison(child);

src/React/.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.gitattributes export-ignore
2+
/.gitignore export-ignore
3+
/phpunit.xml.dist export-ignore
4+
/Resources/assets/test export-ignore
5+
/Tests export-ignore

src/React/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor
2+
composer.lock
3+
.php_cs.cache
4+
.phpunit.result.cache

src/React/.symfony.bundle.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
branches: ["2.x"]
2+
maintained_branches: ["2.x"]
3+
doc_dir: "Resources/doc"

src/React/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CHANGELOG
2+
3+
## 2.2
4+
5+
- Component added
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\React\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
18+
use Symfony\UX\React\Twig\ReactComponentExtension;
19+
20+
/**
21+
* @author Titouan Galopin <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
class ReactExtension extends Extension
26+
{
27+
public function load(array $configs, ContainerBuilder $container)
28+
{
29+
$container
30+
->setDefinition('twig.extension.react', new Definition(ReactComponentExtension::class))
31+
->setArgument(0, new Reference('webpack_encore.twig_stimulus_extension'))
32+
->addTag('twig.extension')
33+
->setPublic(false)
34+
;
35+
}
36+
}

src/React/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020-2021 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

src/React/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Symfony UX React
2+
3+
Symfony UX React integrates [React](https://reactjs.org/) into Symfony applications.
4+
It provides tools to render React components from Twig.
5+
6+
**This repository is a READ-ONLY sub-tree split**. See
7+
https://github.com/symfony/ux to create issues or submit pull requests.
8+
9+
## Resources
10+
11+
- [Documentation](https://symfony.com/bundles/ux-react/current/index.html)
12+
- [Report issues](https://github.com/symfony/ux/issues) and
13+
[send Pull Requests](https://github.com/symfony/ux/pulls)
14+
in the [main Symfony UX repository](https://github.com/symfony/ux)

src/React/ReactBundle.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\React;
13+
14+
use Symfony\Component\HttpKernel\Bundle\Bundle;
15+
16+
/**
17+
* @author Titouan Galopin <[email protected]>
18+
*
19+
* @final
20+
* @experimental
21+
*/
22+
class ReactBundle extends Bundle
23+
{
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function registerReactControllerComponents(context) {
2+
const reactControllers = {};
3+
const importAllReactComponents = (r) => {
4+
r.keys().forEach((key) => (reactControllers[key] = r(key).default));
5+
};
6+
importAllReactComponents(context);
7+
window.resolveReactComponent = (name) => {
8+
const component = reactControllers['./' + name + '.jsx'] || reactControllers['./' + name + '.tsx'];
9+
if (typeof component === 'undefined') {
10+
throw new Error('React controller "' + name + '" does not exist');
11+
}
12+
return component;
13+
};
14+
}
15+
16+
export { registerReactControllerComponents };
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import { Controller } from '@hotwired/stimulus';
3+
4+
class default_1 extends Controller {
5+
connect() {
6+
this._dispatchEvent('react:connect', { component: this.componentValue, props: this.propsValue });
7+
const component = window.resolveReactComponent(this.componentValue);
8+
this._renderReactElement(React.createElement(component, this.propsValue, null));
9+
this._dispatchEvent('react:mount', {
10+
componentName: this.componentValue,
11+
component: component,
12+
props: this.propsValue,
13+
});
14+
}
15+
disconnect() {
16+
this.element.unmount();
17+
this._dispatchEvent('react:unmount', { component: this.componentValue, props: this.propsValue });
18+
}
19+
_renderReactElement(reactElement) {
20+
if (parseInt(React.version) >= 18) {
21+
const root = require('react-dom/client').createRoot(this.element);
22+
root.render(reactElement);
23+
this.element.unmount = () => {
24+
root.unmount();
25+
};
26+
return;
27+
}
28+
const reactDom = require('react-dom');
29+
reactDom.render(reactElement, this.element);
30+
this.element.unmount = () => {
31+
reactDom.unmountComponentAtNode(this.element);
32+
};
33+
}
34+
_dispatchEvent(name, payload) {
35+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
36+
}
37+
}
38+
default_1.values = {
39+
component: String,
40+
props: Object,
41+
};
42+
43+
export { default_1 as default };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../../../../jest.config.js');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@symfony/ux-react",
3+
"description": "Integration of React in Symfony",
4+
"license": "MIT",
5+
"version": "1.0.0",
6+
"main": "dist/register_controller.js",
7+
"symfony": {
8+
"controllers": {
9+
"react": {
10+
"main": "dist/render_controller.js",
11+
"webpackMode": "eager",
12+
"fetch": "eager",
13+
"enabled": true
14+
}
15+
}
16+
},
17+
"peerDependencies": {
18+
"@hotwired/stimulus": "^3.0.0",
19+
"react": "^18.0",
20+
"react-dom": "^18.0"
21+
},
22+
"devDependencies": {
23+
"@hotwired/stimulus": "^3.0.0",
24+
"@types/react": "^18.0",
25+
"@types/react-dom": "^18.0",
26+
"@types/webpack-env": "^1.16",
27+
"react": "^18.0",
28+
"react-dom": "^18.0"
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
export function registerReactControllerComponents(context: __WebpackModuleApi.RequireContext) {
13+
const reactControllers: { [key: string]: object } = {};
14+
15+
const importAllReactComponents = (r: __WebpackModuleApi.RequireContext) => {
16+
r.keys().forEach((key) => (reactControllers[key] = r(key).default));
17+
};
18+
19+
importAllReactComponents(context);
20+
21+
// Expose a global React loader to allow rendering from the Stimulus controller
22+
(window as any).resolveReactComponent = (name: string): object => {
23+
const component = reactControllers[`./${name}.jsx`] || reactControllers[`./${name}.tsx`];
24+
if (typeof component === 'undefined') {
25+
throw new Error('React controller "' + name + '" does not exist');
26+
}
27+
28+
return component;
29+
};
30+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import React, { ReactElement } from 'react';
13+
import { createRoot } from 'react-dom/client';
14+
import { Controller } from '@hotwired/stimulus';
15+
16+
export default class extends Controller {
17+
readonly componentValue: string;
18+
readonly propsValue: object;
19+
20+
static values = {
21+
component: String,
22+
props: Object,
23+
};
24+
25+
connect() {
26+
this._dispatchEvent('react:connect', { component: this.componentValue, props: this.propsValue });
27+
28+
const component = window.resolveReactComponent(this.componentValue);
29+
this._renderReactElement(React.createElement(component, this.propsValue, null));
30+
31+
this._dispatchEvent('react:mount', {
32+
componentName: this.componentValue,
33+
component: component,
34+
props: this.propsValue,
35+
});
36+
}
37+
38+
disconnect() {
39+
(this.element as any).root.unmount();
40+
this._dispatchEvent('react:unmount', { component: this.componentValue, props: this.propsValue });
41+
}
42+
43+
_renderReactElement(reactElement: ReactElement) {
44+
const root = createRoot(this.element);
45+
root.render(reactElement);
46+
47+
(this.element as any).root = root;
48+
}
49+
50+
_dispatchEvent(name: string, payload: any) {
51+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
52+
}
53+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function () {
2+
return <div>Hello</div>;
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react';
2+
3+
export default function () {
4+
return <div>Hello</div>;
5+
}

0 commit comments

Comments
 (0)