diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml
index 76971211296..59216da3bbc 100644
--- a/.github/workflows/browser-tests.yml
+++ b/.github/workflows/browser-tests.yml
@@ -73,8 +73,6 @@ jobs:
- name: Configure E2E app
run: |
- echo 'APP_ENV=prod' >> .env.local
- echo 'APP_DEBUG=0' >> .env.local
echo 'APP_SECRET=df4c071596e64cc75a349456f2887ae2419ae650' >> .env.local
working-directory: apps/e2e
@@ -83,11 +81,12 @@ jobs:
with:
working-directory: apps/e2e
dependency-versions: highest
- composer-options: --no-dev
custom-cache-suffix: symfony-${{ matrix.symfony }}
- name: Prepare E2E app
run: |
+ echo 'APP_ENV=prod' >> .env.local
+ echo 'APP_DEBUG=0' >> .env.local
symfony composer dump-autoload --classmap-authoritative --no-dev
symfony composer dump-env
symfony console asset-map:compile
diff --git a/apps/e2e/.env b/apps/e2e/.env
index c5badfcd659..ae8e8d47fba 100644
--- a/apps/e2e/.env
+++ b/apps/e2e/.env
@@ -23,7 +23,7 @@ APP_SECRET=
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
-DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
+DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
diff --git a/apps/e2e/.symfony.local.yaml b/apps/e2e/.symfony.local.yaml
index 3d0a7009141..8d425e86b43 100644
--- a/apps/e2e/.symfony.local.yaml
+++ b/apps/e2e/.symfony.local.yaml
@@ -1,2 +1,3 @@
http:
port: 9876
+ no_tls: true
diff --git a/apps/e2e/README.md b/apps/e2e/README.md
index 4795b30333d..1205bf2a9eb 100644
--- a/apps/e2e/README.md
+++ b/apps/e2e/README.md
@@ -1,8 +1,8 @@
# E2E App
-This is a Symfony application designed for end-to-end testing.
+This is a Symfony application designed for end-to-end testing.
-It serves for testing UX packages in a real-world scenario,
+It serves for testing UX packages in a real-world scenario,
to ensure they work as expected for multiple Symfony versions and various browsers.
## Requirements
@@ -16,7 +16,7 @@ to ensure they work as expected for multiple Symfony versions and various browse
```shell
docker compose up -d
-symfony php ../.github/build-packages.php
+symfony php ../../.github/build-packages.php
SYMFONY_REQUIRE=6.4.* symfony composer update
# or...
diff --git a/apps/e2e/assets/app.js b/apps/e2e/assets/app.js
index 5b1ceb0c4e7..a237aff12dd 100644
--- a/apps/e2e/assets/app.js
+++ b/apps/e2e/assets/app.js
@@ -2,6 +2,8 @@ import { registerVueControllerComponents } from '@symfony/ux-vue';
import { registerSvelteControllerComponents } from '@symfony/ux-svelte';
import { registerReactControllerComponents } from '@symfony/ux-react';
import './bootstrap.js';
+import { trans } from "./translator.js";
+
/*
* Welcome to your app's main JavaScript file!
*
@@ -16,3 +18,5 @@ console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
registerReactControllerComponents();
registerSvelteControllerComponents();
registerVueControllerComponents();
+
+export { trans };
diff --git a/apps/e2e/assets/controllers/movie-autocomplete_controller.js b/apps/e2e/assets/controllers/movie-autocomplete_controller.js
new file mode 100644
index 00000000000..be8682958c3
--- /dev/null
+++ b/apps/e2e/assets/controllers/movie-autocomplete_controller.js
@@ -0,0 +1,35 @@
+import { Controller } from '@hotwired/stimulus';
+import { getComponent } from '@symfony/ux-live-component';
+
+export default class extends Controller {
+ async connect() {
+ this.component = await getComponent(this.element.closest('[data-controller*="live"]'));
+
+ this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
+ this.element.addEventListener('autocomplete:connect', this._onConnect.bind(this));
+ }
+
+ disconnect() {
+ this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
+ this.element.removeEventListener('autocomplete:connect', this._onConnect.bind(this));
+ }
+
+ _onPreConnect(event) {
+ const options = event.detail.options;
+ options.render = {
+ ...options.render,
+ option: (item) => {
+ return `
${item.text}
`;
+ },
+ };
+ }
+
+ _onConnect(event) {
+ const tomSelect = event.detail.tomSelect;
+
+ tomSelect.on('item_add', (value, item) => {
+ const title = item.getAttribute('data-title') || item.textContent;
+ this.component.emit('movie-selected', { title });
+ });
+ }
+}
diff --git a/apps/e2e/assets/controllers/videogame-autocomplete_controller.js b/apps/e2e/assets/controllers/videogame-autocomplete_controller.js
new file mode 100644
index 00000000000..0b30e1c1eba
--- /dev/null
+++ b/apps/e2e/assets/controllers/videogame-autocomplete_controller.js
@@ -0,0 +1,35 @@
+import { Controller } from '@hotwired/stimulus';
+import { getComponent } from '@symfony/ux-live-component';
+
+export default class extends Controller {
+ async connect() {
+ this.component = await getComponent(this.element.closest('[data-controller*="live"]'));
+
+ this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
+ this.element.addEventListener('autocomplete:connect', this._onConnect.bind(this));
+ }
+
+ disconnect() {
+ this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
+ this.element.removeEventListener('autocomplete:connect', this._onConnect.bind(this));
+ }
+
+ _onPreConnect(event) {
+ const options = event.detail.options;
+ options.render = {
+ ...options.render,
+ option: (item) => {
+ return `${item.text}
`;
+ },
+ };
+ }
+
+ _onConnect(event) {
+ const tomSelect = event.detail.tomSelect;
+
+ tomSelect.on('item_add', (value, item) => {
+ const title = item.getAttribute('data-title') || item.textContent;
+ this.component.emit('videogame-selected', { title });
+ });
+ }
+}
diff --git a/apps/e2e/assets/icons/mdi/search.svg b/apps/e2e/assets/icons/mdi/search.svg
new file mode 100644
index 00000000000..c5c75c4d193
--- /dev/null
+++ b/apps/e2e/assets/icons/mdi/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/e2e/assets/translator.js b/apps/e2e/assets/translator.js
index a0efa830ae4..320d791650b 100644
--- a/apps/e2e/assets/translator.js
+++ b/apps/e2e/assets/translator.js
@@ -1,5 +1,6 @@
-import { localeFallbacks } from '@app/translations/configuration';
-import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
+import { createTranslator } from "@symfony/ux-translator";
+import { messages, localeFallbacks } from "../var/translations/index.js";
+
/*
* This file is part of the Symfony UX Translator package.
*
@@ -9,8 +10,9 @@ import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-tra
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
*/
-setLocaleFallbacks(localeFallbacks);
-
-export { trans };
+export const translator = createTranslator({
+ messages,
+ localeFallbacks,
+});
-export * from '@app/translations';
+export const { trans, setLocale } = translator;
diff --git a/apps/e2e/composer.json b/apps/e2e/composer.json
index b22dc2bc498..8a868cfc154 100644
--- a/apps/e2e/composer.json
+++ b/apps/e2e/composer.json
@@ -7,7 +7,8 @@
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
- "importmap:install": "symfony-cmd"
+ "importmap:install": "symfony-cmd",
+ "foundry:load-fixtures": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
@@ -37,7 +38,10 @@
"symfony/http-client": "6.4.*|7.3.*",
"symfony/intl": "6.4.*|7.3.*",
"symfony/monolog-bundle": "^3.10",
+ "symfony/property-access": "6.4.*|7.3.*",
+ "symfony/property-info": "6.4.*|7.3.*",
"symfony/runtime": "6.4.*|7.3.*",
+ "symfony/serializer": "6.4.*|7.3.*",
"symfony/stimulus-bundle": "^2.29.1",
"symfony/twig-bundle": "6.4.*|7.3.*",
"symfony/ux-autocomplete": "^2.29.1",
@@ -61,6 +65,7 @@
"symfony/ux-typed": "^2.29.1",
"symfony/ux-vue": "^2.29.1",
"symfony/yaml": "6.4.*|7.3.*",
+ "symfonycasts/dynamic-forms": "^0.2",
"twig/extra-bundle": "^3.21",
"twig/twig": "^3.21.1"
},
@@ -68,7 +73,8 @@
"symfony/debug-bundle": "6.4.*|7.3.*",
"symfony/maker-bundle": "^1.64",
"symfony/stopwatch": "6.4.*|7.3.*",
- "symfony/web-profiler-bundle": "6.4.*|7.3.*"
+ "symfony/web-profiler-bundle": "6.4.*|7.3.*",
+ "zenstruck/foundry": "^2.8"
},
"config": {
"platform": {
diff --git a/apps/e2e/config/bundles.php b/apps/e2e/config/bundles.php
index a90e1a7edd1..0354973075c 100644
--- a/apps/e2e/config/bundles.php
+++ b/apps/e2e/config/bundles.php
@@ -30,4 +30,5 @@
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
Symfony\UX\Typed\TypedBundle::class => ['all' => true],
Symfony\UX\Vue\VueBundle::class => ['all' => true],
+ Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],
];
diff --git a/apps/e2e/config/packages/zenstruck_foundry.yaml b/apps/e2e/config/packages/zenstruck_foundry.yaml
new file mode 100644
index 00000000000..2f60dd01843
--- /dev/null
+++ b/apps/e2e/config/packages/zenstruck_foundry.yaml
@@ -0,0 +1,16 @@
+when@dev: &dev
+ # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
+ zenstruck_foundry:
+ persistence:
+ # Flush only once per call of `PersistentObjectFactory::create()`
+ flush_once: true
+
+ # If you use the `make:factory --test` command, you may need to uncomment the following.
+ # See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#generate
+ #services:
+ # App\Tests\Factory\:
+ # resource: '%kernel.project_dir%/tests/Factory/'
+ # autowire: true
+ # autoconfigure: true
+
+when@test: *dev
diff --git a/apps/e2e/importmap.php b/apps/e2e/importmap.php
index fd47adc87d8..a522562e05b 100644
--- a/apps/e2e/importmap.php
+++ b/apps/e2e/importmap.php
@@ -139,12 +139,6 @@
'@symfony/ux-translator' => [
'path' => './vendor/symfony/ux-translator/assets/dist/translator_controller.js',
],
- '@app/translations' => [
- 'path' => './var/translations/index.js',
- ],
- '@app/translations/configuration' => [
- 'path' => './var/translations/configuration.js',
- ],
'typed.js' => [
'version' => '2.1.0',
],
diff --git a/apps/e2e/public/images/example.jpg b/apps/e2e/public/images/example.jpg
new file mode 100644
index 00000000000..49649fb6ca3
Binary files /dev/null and b/apps/e2e/public/images/example.jpg differ
diff --git a/apps/e2e/src/Controller/AutocompleteController.php b/apps/e2e/src/Controller/AutocompleteController.php
index 42d875fe685..2e1f21d99c3 100644
--- a/apps/e2e/src/Controller/AutocompleteController.php
+++ b/apps/e2e/src/Controller/AutocompleteController.php
@@ -2,14 +2,67 @@
namespace App\Controller;
-use Psr\Log\LoggerInterface;
+use App\Form\FruitAutocompleteField;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
-use Symfony\UX\Chartjs\Model\Chart;
-#[Route('/ux-autocomplete')]
+#[Route('/ux-autocomplete', name: 'app_ux_autocomplete_')]
final class AutocompleteController extends AbstractController
{
+ #[Route('/without-ajax', name: 'without_ajax')]
+ public function withoutAjax(): Response
+ {
+ $formBuilder = $this->createFormBuilder();
+ $formBuilder->add('favorite_fruit', ChoiceType::class, [
+ 'autocomplete' => true,
+ 'label' => 'Your favorite fruit:',
+ 'choices' => [
+ 'Apple' => 'apple',
+ 'Banana' => 'banana',
+ 'Cherry' => 'cherry',
+ 'Coconut' => 'coconut',
+ 'Grape' => 'grape',
+ 'Kiwi' => 'kiwi',
+ 'Lemon' => 'lemon',
+ 'Mango' => 'mango',
+ 'Orange' => 'orange',
+ 'Papaya' => 'papaya',
+ 'Peach' => 'peach',
+ 'Pineapple' => 'pineapple',
+ 'Pear' => 'pear',
+ 'Pomegranate' => 'pomegranate',
+ 'Pomelo' => 'pomelo',
+ 'Raspberry' => 'raspberry',
+ 'Strawberry' => 'strawberry',
+ 'Watermelon' => 'watermelon',
+ ],
+ ]);
+
+ $form = $formBuilder->getForm();
+
+ return $this->render('ux_autocomplete/without_ajax.html.twig', [
+ 'form' => $form->createView()
+ ]);
+ }
+
+ #[Route('/with-ajax', name: 'with_ajax')]
+ public function withAjax(): Response
+ {
+ $formBuilder = $this->createFormBuilder();
+ $formBuilder->add('favorite_fruit', FruitAutocompleteField::class);
+
+ $form = $formBuilder->getForm();
+
+ return $this->render('ux_autocomplete/with_ajax.html.twig', [
+ 'form' => $form->createView()
+ ]);
+ }
+
+ #[Route('/custom-controller', name: 'custom_controller')]
+ public function customController(): Response
+ {
+ return $this->render('ux_autocomplete/custom_controller.html.twig');
+ }
}
diff --git a/apps/e2e/src/Controller/ChartjsController.php b/apps/e2e/src/Controller/ChartjsController.php
index f303b2382c7..f5f63564abe 100644
--- a/apps/e2e/src/Controller/ChartjsController.php
+++ b/apps/e2e/src/Controller/ChartjsController.php
@@ -8,10 +8,10 @@
use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
use Symfony\UX\Chartjs\Model\Chart;
-#[Route('/ux-chartjs')]
+#[Route('/ux-chartjs', name: 'app_ux_chartjs_')]
final class ChartjsController extends AbstractController
{
- #[Route('/without-options')]
+ #[Route('/without-options', name: 'without_options')]
public function withoutOptions(ChartBuilderInterface $chartBuilder): Response
{
$chart = $chartBuilder->createChart(Chart::TYPE_LINE);
@@ -33,7 +33,7 @@ public function withoutOptions(ChartBuilderInterface $chartBuilder): Response
]);
}
- #[Route('/with-options')]
+ #[Route('/with-options', name: 'with_options')]
public function withOptions(ChartBuilderInterface $chartBuilder): Response
{
$chart = $chartBuilder->createChart(Chart::TYPE_LINE);
@@ -58,4 +58,69 @@ public function withOptions(ChartBuilderInterface $chartBuilder): Response
'chart' => $chart,
]);
}
+
+ #[Route('/pie', name: 'pie')]
+ public function pie(ChartBuilderInterface $chartBuilder): Response
+ {
+ $chart = $chartBuilder->createChart(Chart::TYPE_PIE);
+
+ $chart->setData([
+ 'labels' => ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+ 'datasets' => [
+ [
+ 'label' => 'My First Dataset',
+ 'data' => [12, 19, 3, 5, 2, 3],
+ 'backgroundColor' => [
+ 'rgb(255, 99, 132)',
+ 'rgb(54, 162, 235)',
+ 'rgb(255, 205, 86)',
+ 'rgb(75, 192, 192)',
+ 'rgb(153, 102, 255)',
+ 'rgb(255, 159, 64)',
+ ],
+ ],
+ ],
+ ]);
+
+ return $this->render('ux_chartjs/index.html.twig', [
+ 'chart' => $chart,
+ ]);
+ }
+
+ #[Route('/pie-with-options', name: 'pie_with_options')]
+ public function pieWithOptions(ChartBuilderInterface $chartBuilder): Response
+ {
+ $chart = $chartBuilder->createChart(Chart::TYPE_PIE);
+
+ $chart->setData([
+ 'labels' => ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+ 'datasets' => [
+ [
+ 'label' => 'My First Dataset',
+ 'data' => [12, 19, 3, 5, 2, 3],
+ 'backgroundColor' => [
+ 'rgb(255, 99, 132)',
+ 'rgb(54, 162, 235)',
+ 'rgb(255, 205, 86)',
+ 'rgb(75, 192, 192)',
+ 'rgb(153, 102, 255)',
+ 'rgb(255, 159, 64)',
+ ],
+ ],
+ ],
+ ]);
+
+ $chart->setOptions([
+ 'responsive' => true,
+ 'plugins' => [
+ 'legend' => [
+ 'position' => 'top',
+ ],
+ ],
+ ]);
+
+ return $this->render('ux_chartjs/index.html.twig', [
+ 'chart' => $chart,
+ ]);
+ }
}
diff --git a/apps/e2e/src/Controller/CropperjsController.php b/apps/e2e/src/Controller/CropperjsController.php
index cf9363c4dbc..7ea31d9dbac 100644
--- a/apps/e2e/src/Controller/CropperjsController.php
+++ b/apps/e2e/src/Controller/CropperjsController.php
@@ -3,17 +3,77 @@
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
+use Symfony\UX\Cropperjs\Factory\CropperInterface;
+use Symfony\UX\Cropperjs\Form\CropperType;
-#[Route('/ux-cropperjs')]
+#[Route('/ux-cropperjs', name: 'app_ux_cropperjs_')]
final class CropperjsController extends AbstractController
{
- #[Route('/')]
- public function index(): Response
+ public function __construct(
+ #[Autowire('%kernel.project_dir%/public')]
+ private string $publicDir
+ ) {}
+
+ #[Route('/crop', name: 'crop')]
+ public function crop(CropperInterface $cropper, Request $request): Response
{
- return $this->render('ux_cropperjs/index.html.twig', [
- 'controller_name' => 'CropperjsController',
+ $crop = $cropper->createCrop($this->publicDir . '/images/example.jpg');
+ $crop->setCroppedMaxSize(800, 600);
+
+ $form = $this->createFormBuilder(['crop' => $crop])
+ ->add('crop', CropperType::class, [
+ 'public_url' => '/images/example.jpg',
+ 'cropper_options' => [
+ 'viewMode' => 1,
+ ],
+ ])
+ ->getForm();
+
+ $form->handleRequest($request);
+
+ $croppedImageData = null;
+ if ($form->isSubmitted() && $form->isValid()) {
+ // Get the cropped image as base64
+ $croppedImageData = base64_encode($crop->getCroppedImage());
+ }
+
+ return $this->render('ux_cropperjs/crop.html.twig', [
+ 'form' => $form,
+ 'croppedImageData' => $croppedImageData,
+ ]);
+ }
+
+ #[Route('/crop-with-aspect-ratio', name: 'crop_with_aspect_ratio')]
+ public function cropWithAspectRatio(CropperInterface $cropper, Request $request): Response
+ {
+ $crop = $cropper->createCrop($this->publicDir . '/images/example.jpg');
+ $crop->setCroppedMaxSize(1920, 1080);
+
+ $form = $this->createFormBuilder(['crop' => $crop])
+ ->add('crop', CropperType::class, [
+ 'public_url' => '/images/example.jpg',
+ 'cropper_options' => [
+ 'aspectRatio' => 16 / 9,
+ 'viewMode' => 1,
+ ],
+ ])
+ ->getForm();
+
+ $form->handleRequest($request);
+
+ $croppedImageData = null;
+ if ($form->isSubmitted() && $form->isValid()) {
+ // Get the cropped image as base64
+ $croppedImageData = base64_encode($crop->getCroppedImage());
+ }
+
+ return $this->render('ux_cropperjs/crop.html.twig', [
+ 'form' => $form,
+ 'croppedImageData' => $croppedImageData,
]);
}
}
diff --git a/apps/e2e/src/Controller/DropzoneController.php b/apps/e2e/src/Controller/DropzoneController.php
index 1dfe103d6f5..baa4b73bf00 100644
--- a/apps/e2e/src/Controller/DropzoneController.php
+++ b/apps/e2e/src/Controller/DropzoneController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-dropzone')]
+#[Route('/ux-dropzone', name: 'app_ux_dropzone_')]
final class DropzoneController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_dropzone/index.html.twig', [
diff --git a/apps/e2e/src/Controller/HomeController.php b/apps/e2e/src/Controller/HomeController.php
index 31e48e7d8a8..76f7402fa88 100644
--- a/apps/e2e/src/Controller/HomeController.php
+++ b/apps/e2e/src/Controller/HomeController.php
@@ -2,7 +2,6 @@
namespace App\Controller;
-use App\Repository\ExampleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -10,10 +9,8 @@
final class HomeController extends AbstractController
{
#[Route('/', name: 'app_home')]
- public function index(ExampleRepository $exampleRepository): Response
+ public function index(): Response
{
- return $this->render('home.html.twig', [
- 'examples_by_package' => $exampleRepository->findAllByPackage(),
- ]);
+ return $this->render('home.html.twig');
}
}
diff --git a/apps/e2e/src/Controller/IconsController.php b/apps/e2e/src/Controller/IconsController.php
index 81b47258867..b0f0d6c89ef 100644
--- a/apps/e2e/src/Controller/IconsController.php
+++ b/apps/e2e/src/Controller/IconsController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-icons')]
+#[Route('/ux-icons', name: 'app_ux_icons_')]
final class IconsController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_icons/index.html.twig', [
diff --git a/apps/e2e/src/Controller/LiveComponentController.php b/apps/e2e/src/Controller/LiveComponentController.php
index c348f73d733..b85474a3774 100644
--- a/apps/e2e/src/Controller/LiveComponentController.php
+++ b/apps/e2e/src/Controller/LiveComponentController.php
@@ -6,14 +6,68 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-live-component')]
+#[Route('/ux-live-component', name: 'app_ux_live_component_')]
final class LiveComponentController extends AbstractController
{
- #[Route('/')]
- public function index(): Response
+ #[Route('/counter', name: 'counter')]
+ public function counter(): Response
{
- return $this->render('ux_live_component/index.html.twig', [
- 'controller_name' => 'LiveComponentController',
+ return $this->render('ux_live_component/counter.html.twig');
+ }
+
+ #[Route('/registration-form', name: 'registration_form')]
+ public function registrationForm(): Response
+ {
+ return $this->render('ux_live_component/registration_form.html.twig');
+ }
+
+ #[Route('/fruits/{page?1}', name: 'fruits')]
+ public function fruits(int $page): Response
+ {
+ return $this->render('ux_live_component/fruits.html.twig', [
+ 'page' => $page,
]);
}
+
+ #[Route('/with-dto', name: 'with_dto')]
+ public function withDto(): Response
+ {
+ return $this->render('ux_live_component/with_dto.html.twig');
+ }
+
+ #[Route('/with-dto-collection', name: 'with_dto_collection')]
+ public function withDtoCollection(): Response
+ {
+ return $this->render('ux_live_component/with_dto_collection.html.twig');
+ }
+
+ #[Route('/with-dto-and-serializer', name: 'with_dto_and_serializer')]
+ public function withDtoAndSerializer(): Response
+ {
+ return $this->render('ux_live_component/with_dto_and_serializer.html.twig');
+ }
+
+ #[Route('/with-dto-and-custom-hydration-methods', name: 'with_dto_and_custom_hydration_methods')]
+ public function withDtoAndCustomHydrationMethods(): Response
+ {
+ return $this->render('ux_live_component/with_dto_and_custom_hydration_methods.html.twig');
+ }
+
+ #[Route('/with-dto-and-hydration-extension', name: 'with_dto_and_hydration_extension')]
+ public function withDtoAndHydrationExtension(): Response
+ {
+ return $this->render('ux_live_component/with_dto_and_hydration_extension.html.twig');
+ }
+
+ #[Route('/item-list', name: 'item_list')]
+ public function itemList(): Response
+ {
+ return $this->render('ux_live_component/item_list.html.twig');
+ }
+
+ #[Route('/with-aliased-live-props', name: 'with_aliased_live_props')]
+ public function withAliasedLiveProps(): Response
+ {
+ return $this->render('ux_live_component/with_aliased_live_props.html.twig');
+ }
}
diff --git a/apps/e2e/src/Controller/MapController.php b/apps/e2e/src/Controller/MapController.php
index b374a859317..2b0c3589b4a 100644
--- a/apps/e2e/src/Controller/MapController.php
+++ b/apps/e2e/src/Controller/MapController.php
@@ -19,10 +19,10 @@
use Symfony\UX\Map\Polyline;
use Symfony\UX\Map\Rectangle;
-#[Route('/ux-map')]
+#[Route('/ux-map', name: 'app_ux_map_')]
final class MapController extends AbstractController
{
- #[Route('/basic')]
+ #[Route('/basic', name: 'basic')]
public function basic(
#[MapQueryParameter] MapRenderer $renderer
): Response {
@@ -34,7 +34,7 @@ public function basic(
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-markers-and-fit-bounds-to-markers')]
+ #[Route('/with-markers-and-fit-bounds-to-markers', name: 'with_markers_and_fit_bounds_to_markers')]
public function withMarkersAndFitBoundsToMarkers(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -52,7 +52,7 @@ public function withMarkersAndFitBoundsToMarkers(#[MapQueryParameter] MapRendere
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-markers-and-zoomed-on-paris')]
+ #[Route('/with-markers-and-zoomed-on-paris', name: 'with_markers_and_zoomed_on_paris')]
public function withMarkersZoomedOnParis(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -71,7 +71,7 @@ public function withMarkersZoomedOnParis(#[MapQueryParameter] MapRenderer $rende
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-markers-and-info-windows')]
+ #[Route('/with-markers-and-info-windows', name: 'with_markers_and_info_windows')]
public function withMarkersAndInfoWindows(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -91,7 +91,7 @@ public function withMarkersAndInfoWindows(#[MapQueryParameter] MapRenderer $rend
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-markers-and-custom-icons')]
+ #[Route('/with-markers-and-custom-icons', name: 'with_markers_and_custom_icons')]
public function withMarkersAndCustomIcons(
#[MapQueryParameter] MapRenderer $renderer,
#[Autowire(service: 'asset_mapper.asset_package')] PackageInterface $package,
@@ -119,7 +119,7 @@ public function withMarkersAndCustomIcons(
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-polygons')]
+ #[Route('/with-polygons', name: 'with_polygons')]
public function withPolygons(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -160,7 +160,7 @@ public function withPolygons(#[MapQueryParameter] MapRenderer $renderer): Respon
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-polylines')]
+ #[Route('/with-polylines', name: 'with_polylines')]
public function withPolylines(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -192,7 +192,7 @@ public function withPolylines(#[MapQueryParameter] MapRenderer $renderer): Respo
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-circles')]
+ #[Route('/with-circles', name: 'with_circles')]
public function withCircles(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
@@ -217,7 +217,7 @@ public function withCircles(#[MapQueryParameter] MapRenderer $renderer): Respons
return $this->render('ux_map/render_map.html.twig', ['map' => $map]);
}
- #[Route('/with-rectangles')]
+ #[Route('/with-rectangles', name: 'with_rectangles')]
public function withRectangles(#[MapQueryParameter] MapRenderer $renderer): Response
{
$map = (new Map(rendererName: $renderer->value))
diff --git a/apps/e2e/src/Controller/NotifyController.php b/apps/e2e/src/Controller/NotifyController.php
index 9f53da9ade6..a1f81b2d6b2 100644
--- a/apps/e2e/src/Controller/NotifyController.php
+++ b/apps/e2e/src/Controller/NotifyController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-notify')]
+#[Route('/ux-notify', name: 'app_ux_notify_')]
final class NotifyController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_notify/index.html.twig', [
diff --git a/apps/e2e/src/Controller/ReactController.php b/apps/e2e/src/Controller/ReactController.php
index 5f45a282737..8ab8b99527c 100644
--- a/apps/e2e/src/Controller/ReactController.php
+++ b/apps/e2e/src/Controller/ReactController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-react')]
+#[Route('/ux-react', name: 'app_ux_react_')]
final class ReactController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_react/index.html.twig');
diff --git a/apps/e2e/src/Controller/SvelteController.php b/apps/e2e/src/Controller/SvelteController.php
index b17d5bfacb3..e807cbb0d5b 100644
--- a/apps/e2e/src/Controller/SvelteController.php
+++ b/apps/e2e/src/Controller/SvelteController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-svelte')]
+#[Route('/ux-svelte', name: 'app_ux_svelte_')]
final class SvelteController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_svelte/index.html.twig');
diff --git a/apps/e2e/src/Controller/TestAutocompleteController.php b/apps/e2e/src/Controller/TestAutocompleteController.php
new file mode 100644
index 00000000000..9a12f7a6b7c
--- /dev/null
+++ b/apps/e2e/src/Controller/TestAutocompleteController.php
@@ -0,0 +1,63 @@
+render('test/autocomplete_dynamic_form.html.twig');
+ }
+
+ #[Route('/movie', name: 'movie')]
+ public function movie(Request $request): JsonResponse
+ {
+ $query = $request->query->get('query', '');
+
+ $movies = [
+ ['value' => 'movie_1', 'text' => 'The Matrix (1999)', 'title' => 'movie Movie #1'],
+ ['value' => 'movie_2', 'text' => 'Inception (2010)', 'title' => 'movie Movie #2'],
+ ['value' => 'movie_3', 'text' => 'The Dark Knight (2008)', 'title' => 'movie Movie #3'],
+ ['value' => 'movie_4', 'text' => 'Interstellar (2014)', 'title' => 'movie Movie #4'],
+ ['value' => 'movie_5', 'text' => 'Pulp Fiction (1994)', 'title' => 'movie Movie #5'],
+ ];
+
+ $results = array_filter($movies, function ($movie) use ($query) {
+ return '' === $query || false !== stripos($movie['text'], $query);
+ });
+
+ return $this->json([
+ 'results' => array_values($results),
+ ]);
+ }
+
+ #[Route('/videogame', name: 'videogame')]
+ public function videogame(Request $request): JsonResponse
+ {
+ $query = $request->query->get('query', '');
+
+ $games = [
+ ['value' => 'videogame_1', 'text' => 'Halo: Combat Evolved (2001)', 'title' => 'videogame Game #1'],
+ ['value' => 'videogame_2', 'text' => 'The Legend of Zelda (1986)', 'title' => 'videogame Game #2'],
+ ['value' => 'videogame_3', 'text' => 'Half-Life 2 (2004)', 'title' => 'videogame Game #3'],
+ ['value' => 'videogame_4', 'text' => 'Portal (2007)', 'title' => 'videogame Game #4'],
+ ['value' => 'videogame_5', 'text' => 'Mass Effect 2 (2010)', 'title' => 'videogame Game #5'],
+ ];
+
+ $results = array_filter($games, function ($game) use ($query) {
+ return '' === $query || false !== stripos($game['text'], $query);
+ });
+
+ return $this->json([
+ 'results' => array_values($results),
+ ]);
+ }
+}
diff --git a/apps/e2e/src/Controller/TranslatorController.php b/apps/e2e/src/Controller/TranslatorController.php
index 0d3ecf2611c..f09184fbd24 100644
--- a/apps/e2e/src/Controller/TranslatorController.php
+++ b/apps/e2e/src/Controller/TranslatorController.php
@@ -6,52 +6,52 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-translator')]
+#[Route('/ux-translator', name: 'app_ux_translator_')]
final class TranslatorController extends AbstractController
{
- #[Route('/basic')]
+ #[Route('/basic', name: 'basic')]
public function basic(): Response
{
return $this->render('ux_translator/basic.html.twig');
}
- #[Route('/with-parameter')]
+ #[Route('/with-parameter', name: 'with_parameter')]
public function withParameter(): Response
{
return $this->render('ux_translator/with_parameter.html.twig');
}
- #[Route('/icu-select')]
+ #[Route('/icu-select', name: 'icu_select')]
public function icuSelect(): Response
{
return $this->render('ux_translator/icu_select.html.twig');
}
- #[Route('/icu-plural')]
+ #[Route('/icu-plural', name: 'icu_plural')]
public function icuPlural(): Response
{
return $this->render('ux_translator/icu_plural.html.twig');
}
- #[Route('/icu-selectordinal')]
+ #[Route('/icu-selectordinal', name: 'icu_selectordinal')]
public function icuSelectOrdinal(): Response
{
return $this->render('ux_translator/icu_selectordinal.html.twig');
}
- #[Route('/icu-date-time')]
+ #[Route('/icu-date-time', name: 'icu_date_time')]
public function icuDateTime(): Response
{
return $this->render('ux_translator/icu_date_time.html.twig');
}
- #[Route('/icu-number-percent')]
+ #[Route('/icu-number-percent', name: 'icu_number_percent')]
public function icuNumberPercent(): Response
{
return $this->render('ux_translator/icu_number_percent.html.twig');
}
- #[Route('/icu-number-currency')]
+ #[Route('/icu-number-currency', name: 'icu_number_currency')]
public function icuNumberCurrency(): Response
{
return $this->render('ux_translator/icu_number_currency.html.twig');
diff --git a/apps/e2e/src/Controller/TurboController.php b/apps/e2e/src/Controller/TurboController.php
index 52646713c9b..9b3ec0b5db6 100644
--- a/apps/e2e/src/Controller/TurboController.php
+++ b/apps/e2e/src/Controller/TurboController.php
@@ -3,17 +3,57 @@
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Attribute\Route;
+use Symfony\UX\Turbo\TurboBundle;
-#[Route('/ux-turbo')]
+#[Route('/ux-turbo', name: 'app_ux_turbo_')]
final class TurboController extends AbstractController
{
- #[Route('/')]
- public function index(): Response
+
+ #[Route('/drive', name: 'drive')]
+ public function drive(
+ #[MapQueryParameter] int $page = 1,
+ ): Response
{
- return $this->render('ux_turbo/index.html.twig', [
- 'controller_name' => 'TurboController',
+ if ($page === 2) {
+ return $this->render('ux_turbo/drive_page_2.html.twig', [
+ 'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
+ ]);
+ }
+
+ return $this->render('ux_turbo/drive.html.twig', [
+ 'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
]);
}
+
+
+ #[Route('/frame', name: 'frame')]
+ public function frame(): Response
+ {
+ return $this->render('ux_turbo/frame.html.twig');
+ }
+
+ #[Route('/frame-content', name: 'frame_content')]
+ public function frameContent(): Response
+ {
+ return $this->render('ux_turbo/frame_content.html.twig');
+ }
+
+ #[Route('/stream', name: 'stream')]
+ public function streamAction(Request $request): Response
+ {
+ if ($request->isMethod('POST')) {
+ if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
+ $request->setRequestFormat(TurboBundle::STREAM_FORMAT);
+ return $this->render('ux_turbo/stream_response.html.twig');
+ }
+
+ return $this->redirectToRoute('app_ux_turbo_stream');
+ }
+
+ return $this->render('ux_turbo/stream.html.twig');
+ }
}
diff --git a/apps/e2e/src/Controller/TwigComponentController.php b/apps/e2e/src/Controller/TwigComponentController.php
index ffd489a0ae7..05f0e103204 100644
--- a/apps/e2e/src/Controller/TwigComponentController.php
+++ b/apps/e2e/src/Controller/TwigComponentController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-twig-component')]
+#[Route('/ux-twig-component', name: 'app_ux_twig_component_')]
final class TwigComponentController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_twig_component/index.html.twig', [
diff --git a/apps/e2e/src/Controller/TypedController.php b/apps/e2e/src/Controller/TypedController.php
index 1d9c76906ec..00999ab3604 100644
--- a/apps/e2e/src/Controller/TypedController.php
+++ b/apps/e2e/src/Controller/TypedController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-typed')]
+#[Route('/ux-typed', name: 'app_ux_typed_')]
final class TypedController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_typed/index.html.twig', [
diff --git a/apps/e2e/src/Controller/VueController.php b/apps/e2e/src/Controller/VueController.php
index 938858ea9c0..3988b4f66a7 100644
--- a/apps/e2e/src/Controller/VueController.php
+++ b/apps/e2e/src/Controller/VueController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-vue')]
+#[Route('/ux-vue', name: 'app_ux_vue_')]
final class VueController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_vue/index.html.twig');
diff --git a/apps/e2e/src/Entity/Fruit.php b/apps/e2e/src/Entity/Fruit.php
new file mode 100644
index 00000000000..3ad9f798b9a
--- /dev/null
+++ b/apps/e2e/src/Entity/Fruit.php
@@ -0,0 +1,36 @@
+id = $id;
+ $fruit->name = $name;
+
+ return $fruit;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
diff --git a/apps/e2e/src/EventListener/ResolveExampleForUrlListener.php b/apps/e2e/src/EventListener/ResolveExampleForUrlListener.php
index 1b056a18c0d..79f203e19bd 100644
--- a/apps/e2e/src/EventListener/ResolveExampleForUrlListener.php
+++ b/apps/e2e/src/EventListener/ResolveExampleForUrlListener.php
@@ -30,7 +30,7 @@ public function __invoke(RequestEvent $event): void
return;
}
- $example = $this->exampleRepository->findOneByUrl($event->getRequest()->getRequestUri());
+ $example = $this->exampleRepository->findOneByRoute($event->getRequest()->attributes->get('_route'));
$event->getRequest()->attributes->set('_example', $example);
}
}
diff --git a/apps/e2e/src/Example.php b/apps/e2e/src/Example.php
index 9ecade603f4..4adab0b2597 100644
--- a/apps/e2e/src/Example.php
+++ b/apps/e2e/src/Example.php
@@ -17,7 +17,8 @@ public function __construct(
public UxPackage $uxPackage,
public string $name,
public string $description,
- public string $url
+ public string $routeName,
+ public array $routeParameters = [],
) {
}
}
diff --git a/apps/e2e/src/Factory/FruitFactory.php b/apps/e2e/src/Factory/FruitFactory.php
new file mode 100644
index 00000000000..b30b525f577
--- /dev/null
+++ b/apps/e2e/src/Factory/FruitFactory.php
@@ -0,0 +1,36 @@
+
+ */
+final class FruitFactory extends PersistentProxyObjectFactory
+{
+ public static function class(): string
+ {
+ return Fruit::class;
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
+ */
+ protected function defaults(): array|callable
+ {
+ return [];
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
+ */
+ protected function initialize(): static
+ {
+ return $this
+ ->instantiateWith(Instantiator::namedConstructor('create'))
+ ;
+ }
+}
diff --git a/apps/e2e/src/Form/FruitAutocompleteField.php b/apps/e2e/src/Form/FruitAutocompleteField.php
new file mode 100644
index 00000000000..33d0d1776d5
--- /dev/null
+++ b/apps/e2e/src/Form/FruitAutocompleteField.php
@@ -0,0 +1,28 @@
+setDefaults([
+ 'class' => Fruit::class,
+ 'placeholder' => 'Choose a Fruit',
+ 'choice_value' => static fn (?Fruit $fruit) => $fruit?->getId(),
+ 'choice_label' => static fn (Fruit $fruit) => $fruit->getName(),
+ ]);
+ }
+
+ public function getParent(): string
+ {
+ return BaseEntityAutocompleteType::class;
+ }
+}
diff --git a/apps/e2e/src/Form/Model/ProductionDto.php b/apps/e2e/src/Form/Model/ProductionDto.php
new file mode 100644
index 00000000000..8c6f2c028d2
--- /dev/null
+++ b/apps/e2e/src/Form/Model/ProductionDto.php
@@ -0,0 +1,14 @@
+setDefaults([
+ 'autocomplete' => true,
+ 'autocomplete_url' => $this->urlGenerator->generate('app_test_autocomplete_movie'),
+ 'tom_select_options' => [
+ 'maxOptions' => null,
+ ],
+ 'attr' => [
+ 'data-test-id' => 'movie-autocomplete',
+ 'data-controller' => 'movie-autocomplete',
+ ],
+ ]);
+ }
+
+ public function getParent(): string
+ {
+ return TextType::class;
+ }
+}
diff --git a/apps/e2e/src/Form/Type/ProductionType.php b/apps/e2e/src/Form/Type/ProductionType.php
new file mode 100644
index 00000000000..137148e56df
--- /dev/null
+++ b/apps/e2e/src/Form/Type/ProductionType.php
@@ -0,0 +1,66 @@
+add('type', ChoiceType::class, [
+ 'choices' => [
+ 'Movie' => 'movie',
+ 'Videogame' => 'videogame',
+ ],
+ 'placeholder' => 'Select a type',
+ 'attr' => [
+ 'data-test-id' => 'production-type',
+ ],
+ ])
+ ->addDependent('movieSearch', ['type'], function (DependentField $field, ?string $type) {
+ if ('movie' !== $type) {
+ return;
+ }
+
+ $field->add(MovieAutocompleteType::class, [
+ 'label' => 'Search Movies',
+ 'required' => false,
+ ]);
+ })
+ ->addDependent('videogameSearch', ['type'], function (DependentField $field, ?string $type) {
+ if ('videogame' !== $type) {
+ return;
+ }
+
+ $field->add(VideogameAutocompleteType::class, [
+ 'label' => 'Search Videogames',
+ 'required' => false,
+ ]);
+ })
+ ->add('title', TextType::class, [
+ 'required' => false,
+ 'attr' => [
+ 'data-test-id' => 'production-title',
+ ],
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => ProductionDto::class,
+ ]);
+ }
+}
diff --git a/apps/e2e/src/Form/Type/VideogameAutocompleteType.php b/apps/e2e/src/Form/Type/VideogameAutocompleteType.php
new file mode 100644
index 00000000000..dcd7abb2f4b
--- /dev/null
+++ b/apps/e2e/src/Form/Type/VideogameAutocompleteType.php
@@ -0,0 +1,36 @@
+setDefaults([
+ 'autocomplete' => true,
+ 'autocomplete_url' => $this->urlGenerator->generate('app_test_autocomplete_videogame'),
+ 'tom_select_options' => [
+ 'maxOptions' => null,
+ ],
+ 'attr' => [
+ 'data-test-id' => 'videogame-autocomplete',
+ 'data-controller' => 'videogame-autocomplete',
+ ],
+ ]);
+ }
+
+ public function getParent(): string
+ {
+ return TextType::class;
+ }
+}
diff --git a/apps/e2e/src/Hydration/PointHydrationExtension.php b/apps/e2e/src/Hydration/PointHydrationExtension.php
new file mode 100644
index 00000000000..cd9c7af561f
--- /dev/null
+++ b/apps/e2e/src/Hydration/PointHydrationExtension.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Hydration;
+
+use App\Model\Point;
+use Symfony\UX\LiveComponent\Hydration\HydrationExtensionInterface;
+
+/**
+ * @template TData of Point
+ * @template TDehydrated of array{point-x: float; point-y: float}
+ */
+class PointHydrationExtension implements HydrationExtensionInterface
+{
+ public function supports(string $className): bool
+ {
+ return is_a($className, Point::class, true);
+ }
+
+ /**
+ * @param TDehydrated $value
+ * @return null|TData
+ */
+ public function hydrate(mixed $value, string $className): ?object
+ {
+ return Point::create($value['px'], $value['py']);
+ }
+
+ /**
+ * @param TData $object
+ * @return TDehydrated
+ */
+ public function dehydrate(object $object): mixed
+ {
+ return [
+ 'px' => $object->x,
+ 'py' => $object->y,
+ ];
+ }
+}
diff --git a/apps/e2e/src/Model/Address.php b/apps/e2e/src/Model/Address.php
new file mode 100644
index 00000000000..58e600c05e1
--- /dev/null
+++ b/apps/e2e/src/Model/Address.php
@@ -0,0 +1,18 @@
+country = $country;
+ $address->city = $city;
+
+ return $address;
+ }
+}
diff --git a/apps/e2e/src/Model/Point.php b/apps/e2e/src/Model/Point.php
new file mode 100644
index 00000000000..cad0cec6bce
--- /dev/null
+++ b/apps/e2e/src/Model/Point.php
@@ -0,0 +1,18 @@
+x = $x;
+ $point->y = $y;
+
+ return $point;
+ }
+}
diff --git a/apps/e2e/src/Normalizer/AddressNormalizer.php b/apps/e2e/src/Normalizer/AddressNormalizer.php
new file mode 100644
index 00000000000..b84328741da
--- /dev/null
+++ b/apps/e2e/src/Normalizer/AddressNormalizer.php
@@ -0,0 +1,45 @@
+ $data->country,
+ 'serialized_city' => $data->city,
+ ];
+ }
+
+ public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
+ {
+ return $type === Address::class;
+ }
+
+ public function denormalize($data, string $type, ?string $format = null, array $context = []): object
+ {
+ return Address::create(
+ country: $data['serialized_country'],
+ city: $data['serialized_city'],
+ );
+ }
+
+ public function getSupportedTypes(?string $format): array
+ {
+ return [Address::class => true];
+ }
+}
\ No newline at end of file
diff --git a/apps/e2e/src/Repository/ExampleRepository.php b/apps/e2e/src/Repository/ExampleRepository.php
index 51f3e2a3f9b..61379893b09 100644
--- a/apps/e2e/src/Repository/ExampleRepository.php
+++ b/apps/e2e/src/Repository/ExampleRepository.php
@@ -21,37 +21,60 @@ class ExampleRepository
*/
private array $examples;
- public function __construct() {
+ public function __construct()
+ {
$this->examples = [
- new Example(UxPackage::Map, 'Basic map (Leaflet)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=leaflet'),
- new Example(UxPackage::Map, 'Basic map (Google)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=google'),
- new Example(UxPackage::Map, 'With markers, fit bounds (Leaflet)', 'A map with 2 markers, and the bounds are automatically adjusted to fit both markers', '/ux-map/with-markers-and-fit-bounds-to-markers?renderer=leaflet'),
- new Example(UxPackage::Map, 'With markers, fit bounds (Google)', 'A map with 2 markers, and the bounds are automatically adjusted to fit both markers', '/ux-map/with-markers-and-fit-bounds-to-markers?renderer=google'),
- new Example(UxPackage::Map, 'With markers, zoomed on Paris (Leaflet)', 'A map with 2 markers (Paris and Lyon), zoomed on Paris', '/ux-map/with-markers-and-zoomed-on-paris?renderer=leaflet'),
- new Example(UxPackage::Map, 'With markers, zoomed on Paris (Google)', 'A map with 2 markers (Paris and Lyon), zoomed on Paris', '/ux-map/with-markers-and-zoomed-on-paris?renderer=google'),
- new Example(UxPackage::Map, 'With markers and info windows (Leaflet)', 'A map with 2 markers (Paris and Lyon), each with an info window', '/ux-map/with-markers-and-info-windows?renderer=leaflet'),
- new Example(UxPackage::Map, 'With markers and info windows (Google)', 'A map with 2 markers (Paris and Lyon), each with an info window', '/ux-map/with-markers-and-info-windows?renderer=google'),
- new Example(UxPackage::Map, 'With custom icon markers (Leaflet)', 'A map with 3 markers (Paris, Lyon, Bordeaux), each with a custom icon', '/ux-map/with-markers-and-custom-icons?renderer=leaflet'),
- new Example(UxPackage::Map, 'With custom icon markers (Google)', 'A map with 3 markers (Paris, Lyon, Bordeaux), each with a custom icon', '/ux-map/with-markers-and-custom-icons?renderer=google'),
- new Example(UxPackage::Map, 'With polygons (Leaflet)', 'A map with two polygons, one that covers main cities in Italy, and one weird shape on France', '/ux-map/with-polygons?renderer=leaflet'),
- new Example(UxPackage::Map, 'With polygons (Google)', 'A map with two polygons, one that covers main cities in Italy, and one weird shape on France', '/ux-map/with-polygons?renderer=google'),
- new Example(UxPackage::Map, 'With polylines (Leaflet)', 'A map with two polylines: one through Paris/Lyon/Marseille/Bordeaux, and the other one through Rennes/Nantes/Tours', '/ux-map/with-polylines?renderer=leaflet'),
- new Example(UxPackage::Map, 'With polylines (Google)', 'A map with two polylines: one through Paris/Lyon/Marseille/Bordeaux, and the other one through Rennes/Nantes/Tours', '/ux-map/with-polylines?renderer=google'),
- new Example(UxPackage::Map, 'With circles (Leaflet)', 'A map with two circles: one centered on Paris, the other on Lyon', '/ux-map/with-circles?renderer=leaflet'),
- new Example(UxPackage::Map, 'With circles (Google)', 'A map with two circles: one centered on Paris, the other on Lyon', '/ux-map/with-circles?renderer=google'),
- new Example(UxPackage::Map, 'With rectangles (Leaflet)', 'A map with two rectangles: one from Paris to Lille, the other from Lyon to Bordeaux', '/ux-map/with-rectangles?renderer=leaflet'),
- new Example(UxPackage::Map, 'With rectangles (Google)', 'A map with two rectangles: one from Paris to Lille, the other from Lyon to Bordeaux', '/ux-map/with-rectangles?renderer=google'),
- new Example(UxPackage::React, 'Basic React Component', 'A basic React component that displays a welcoming message', '/ux-react/'),
- new Example(UxPackage::Svelte, 'Basic Svelte Component', 'A basic Svelte component that displays a welcoming message', '/ux-svelte/'),
- new Example(UxPackage::Translator, 'Basic translation', 'A simple translation example using the Translator component', '/ux-translator/basic'),
- new Example(UxPackage::Translator, 'Translation with parameter', 'Translation example with dynamic parameters', '/ux-translator/with-parameter'),
- new Example(UxPackage::Translator, 'ICU Translation with `select` argument', 'ICU message format example using the `select` argument', '/ux-translator/icu-select'),
- new Example(UxPackage::Translator, 'ICU Translation with `plural` argument', 'ICU message format example using the `plural` argument', '/ux-translator/icu-plural'),
- new Example(UxPackage::Translator, 'ICU Translation with `selectordinal` argument', 'ICU message format example using the `selectordinal` argument', '/ux-translator/icu-selectordinal'),
- new Example(UxPackage::Translator, 'ICU Translation with `date` and `time` arguments', 'ICU message format example using `date` and `time` arguments', '/ux-translator/icu-date-time'),
- new Example(UxPackage::Translator, 'ICU Translation with `number` and `percent` arguments', 'ICU message format example using `number` and `percent` arguments', '/ux-translator/icu-number-percent'),
- new Example(UxPackage::Translator, 'ICU Translation with `number` and `currency` arguments', 'ICU message format example using `number` and `currency` arguments', '/ux-translator/icu-number-currency'),
- new Example(UxPackage::Vue, 'Basic Vue Component', 'A basic Vue component that displays a welcoming message', '/ux-vue/'),
+ new Example(UxPackage::Autocomplete, 'Autocomplete (without AJAX)', 'An autocomplete form field, by using the choses from the choice type field.', 'app_ux_autocomplete_without_ajax'),
+ new Example(UxPackage::Autocomplete, 'Autocomplete (custom controller)', 'An autocomplete form field, with a custom Stimulus controller for AJAX results.', 'app_ux_autocomplete_custom_controller'),
+ new Example(UxPackage::ChartJs, 'Line chart without options', 'A basic line chart displaying monthly data without additional options.', 'app_ux_chartjs_without_options'),
+ new Example(UxPackage::ChartJs, 'Line chart with options', 'A line chart with custom options (showLines: false) that displays data points without connecting lines.', 'app_ux_chartjs_with_options'),
+ new Example(UxPackage::ChartJs, 'Pie chart', 'A pie chart displaying data distribution across different categories.', 'app_ux_chartjs_pie'),
+ new Example(UxPackage::ChartJs, 'Pie chart with options', 'A pie chart with custom options to control the appearance and behavior.', 'app_ux_chartjs_pie_with_options'),
+ new Example(UxPackage::Cropperjs, 'Image cropper', 'Crop an image with Cropper.js using default options.', 'app_ux_cropperjs_crop'),
+ new Example(UxPackage::Cropperjs, 'Image cropper with aspect ratio', 'Crop an image with a fixed 16:9 aspect ratio constraint.', 'app_ux_cropperjs_crop_with_aspect_ratio'),
+ new Example(UxPackage::LiveComponent, 'Examples filtering', "On this page, you can filter all examples by query terms, and observe how the UI and URLs update during and after processing.", 'app_home'),
+ new Example(UxPackage::LiveComponent, 'Counter', 'A basic counter that you can increment or decrement.', 'app_ux_live_component_counter'),
+ new Example(UxPackage::Turbo, 'Turbo Drive navigation', 'Navigate between pages without full page reload using Turbo Drive.', 'app_ux_turbo_drive'),
+ new Example(UxPackage::Turbo, 'Turbo Frame', 'A scoped section that navigates independently from the rest of the page.', 'app_ux_turbo_frame'),
+ new Example(UxPackage::Turbo, 'Turbo Stream after form submit', 'Update page content with Turbo Streams after a form submission.', 'app_ux_turbo_stream'),
+ new Example(UxPackage::LiveComponent, 'Registration form', 'A registration form with live validation using Symfony Forms and the Validator component.', 'app_ux_live_component_registration_form'),
+ new Example(UxPackage::LiveComponent, 'Paginated fruits list', 'A paginated list of fruits, where the current page is persisted in the URL as a path parameter.', 'app_ux_live_component_fruits'),
+ new Example(UxPackage::LiveComponent, 'With DTO', 'A live component that uses a DTO to encapsulate its state.', 'app_ux_live_component_with_dto'),
+ new Example(UxPackage::LiveComponent, 'With DTO Collection', 'A live component that uses a collection of Data Transfer Objects (DTOs) to encapsulate its state.', 'app_ux_live_component_with_dto_collection'),
+ new Example(UxPackage::LiveComponent, 'With DTO and Serializer', 'A live component that uses a DTO along with the Symfony Serializer component.', 'app_ux_live_component_with_dto_and_serializer'),
+ new Example(UxPackage::LiveComponent, 'With DTO and custom Hydration/Dehydration methods', 'A live component that uses a DTO along with custom methods to hydrate/dehydrate the DTO.', 'app_ux_live_component_with_dto_and_custom_hydration_methods'),
+ new Example(UxPackage::LiveComponent, 'With DTO and dedicated HydrationExtension', 'A live component that uses a DTO along with dedicated HydrationExtension to hydrate/dehydrate the DTO.', 'app_ux_live_component_with_dto_and_hydration_extension'),
+ new Example(UxPackage::LiveComponent, 'Item list', 'A live component with LiveProp, LiveAction and LiveArg.', 'app_ux_live_component_item_list'),
+ new Example(UxPackage::LiveComponent, 'With aliased LiveProps', 'A live component with LiveProps statically and dynamically aliased.', 'app_ux_live_component_with_aliased_live_props'),
+ new Example(UxPackage::Map, 'Basic map (Leaflet)', 'A basic map centered on Paris with zoom level 12', 'app_ux_map_basic', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'Basic map (Google)', 'A basic map centered on Paris with zoom level 12', 'app_ux_map_basic', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With markers, fit bounds (Leaflet)', 'A map with 2 markers, and the bounds are automatically adjusted to fit both markers', 'app_ux_map_with_markers_and_fit_bounds_to_markers', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With markers, fit bounds (Google)', 'A map with 2 markers, and the bounds are automatically adjusted to fit both markers', 'app_ux_map_with_markers_and_fit_bounds_to_markers', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With markers, zoomed on Paris (Leaflet)', 'A map with 2 markers (Paris and Lyon), zoomed on Paris', 'app_ux_map_with_markers_and_zoomed_on_paris', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With markers, zoomed on Paris (Google)', 'A map with 2 markers (Paris and Lyon), zoomed on Paris', 'app_ux_map_with_markers_and_zoomed_on_paris', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With markers and info windows (Leaflet)', 'A map with 2 markers (Paris and Lyon), each with an info window', 'app_ux_map_with_markers_and_info_windows', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With markers and info windows (Google)', 'A map with 2 markers (Paris and Lyon), each with an info window', 'app_ux_map_with_markers_and_info_windows', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With custom icon markers (Leaflet)', 'A map with 3 markers (Paris, Lyon, Bordeaux), each with a custom icon', 'app_ux_map_with_markers_and_custom_icons', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With custom icon markers (Google)', 'A map with 3 markers (Paris, Lyon, Bordeaux), each with a custom icon', 'app_ux_map_with_markers_and_custom_icons', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With polygons (Leaflet)', 'A map with two polygons, one that covers main cities in Italy, and one weird shape on France', 'app_ux_map_with_polygons', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With polygons (Google)', 'A map with two polygons, one that covers main cities in Italy, and one weird shape on France', 'app_ux_map_with_polygons', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With polylines (Leaflet)', 'A map with two polylines: one through Paris/Lyon/Marseille/Bordeaux, and the other one through Rennes/Nantes/Tours', 'app_ux_map_with_polylines', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With polylines (Google)', 'A map with two polylines: one through Paris/Lyon/Marseille/Bordeaux, and the other one through Rennes/Nantes/Tours', 'app_ux_map_with_polylines', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With circles (Leaflet)', 'A map with two circles: one centered on Paris, the other on Lyon', 'app_ux_map_with_circles', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With circles (Google)', 'A map with two circles: one centered on Paris, the other on Lyon', 'app_ux_map_with_circles', ['renderer' => 'google']),
+ new Example(UxPackage::Map, 'With rectangles (Leaflet)', 'A map with two rectangles: one from Paris to Lille, the other from Lyon to Bordeaux', 'app_ux_map_with_rectangles', ['renderer' => 'leaflet']),
+ new Example(UxPackage::Map, 'With rectangles (Google)', 'A map with two rectangles: one from Paris to Lille, the other from Lyon to Bordeaux', 'app_ux_map_with_rectangles', ['renderer' => 'google']),
+ new Example(UxPackage::React, 'Basic React Component', 'A basic React component that displays a welcoming message', 'app_ux_react_index'),
+ new Example(UxPackage::Svelte, 'Basic Svelte Component', 'A basic Svelte component that displays a welcoming message', 'app_ux_svelte_index'),
+ new Example(UxPackage::Translator, 'Basic translation', 'A simple translation example using the Translator component', 'app_ux_translator_basic'),
+ new Example(UxPackage::Translator, 'Translation with parameter', 'Translation example with dynamic parameters', 'app_ux_translator_with_parameter'),
+ new Example(UxPackage::Translator, 'ICU Translation with `select` argument', 'ICU message format example using the `select` argument', 'app_ux_translator_icu_select'),
+ new Example(UxPackage::Translator, 'ICU Translation with `plural` argument', 'ICU message format example using the `plural` argument', 'app_ux_translator_icu_plural'),
+ new Example(UxPackage::Translator, 'ICU Translation with `selectordinal` argument', 'ICU message format example using the `selectordinal` argument', 'app_ux_translator_icu_selectordinal'),
+ new Example(UxPackage::Translator, 'ICU Translation with `date` and `time` arguments', 'ICU message format example using `date` and `time` arguments', 'app_ux_translator_icu_date_time'),
+ new Example(UxPackage::Translator, 'ICU Translation with `number` and `percent` arguments', 'ICU message format example using `number` and `percent` arguments', 'app_ux_translator_icu_number_percent'),
+ new Example(UxPackage::Translator, 'ICU Translation with `number` and `currency` arguments', 'ICU message format example using `number` and `currency` arguments', 'app_ux_translator_icu_number_currency'),
+ new Example(UxPackage::Vue, 'Basic Vue Component', 'A basic Vue component that displays a welcoming message', 'app_ux_vue_index'),
];
}
@@ -63,21 +86,32 @@ public function findAll(): array
return $this->examples;
}
- public function findAllByPackage(): array
+ /**
+ * @return array>
+ */
+ public function findAllGroupedByPackage(string|null $query = null): array
{
$grouped = [];
+ $examples = $this->examples;
- foreach ($this->examples as $example) {
+ if (null !== $query) {
+ $query = strtolower($query);
+ $examples = array_filter($examples,
+ fn(Example $example) => false !== mb_stripos($example->uxPackage->name . ' ' . $example->name . ' ' . $example->description, $query)
+ );
+ }
+
+ foreach ($examples as $example) {
$grouped[$example->uxPackage->value][] = $example;
}
return $grouped;
}
- public function findOneByUrl(string $url): ?Example
+ public function findOneByRoute(string $routeName): ?Example
{
foreach ($this->examples as $example) {
- if ($example->url === $url) {
+ if ($example->routeName === $routeName) {
return $example;
}
}
diff --git a/apps/e2e/src/Repository/FruitRepository.php b/apps/e2e/src/Repository/FruitRepository.php
new file mode 100644
index 00000000000..6d7bf4f12f8
--- /dev/null
+++ b/apps/e2e/src/Repository/FruitRepository.php
@@ -0,0 +1,32 @@
+
+ */
+class FruitRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Fruit::class);
+ }
+
+ /**
+ * @param positive-int $page
+ * @param positive-int $perPage
+ * @return Fruit[]
+ */
+ public function paginate(int $page, int $perPage): array
+ {
+ return $this->createQueryBuilder('f')
+ ->setFirstResult(($page - 1) * $perPage)
+ ->setMaxResults($perPage)
+ ->getQuery()
+ ->getResult();
+ }
+}
diff --git a/apps/e2e/src/Story/AppStory.php b/apps/e2e/src/Story/AppStory.php
new file mode 100644
index 00000000000..07b1bc46459
--- /dev/null
+++ b/apps/e2e/src/Story/AppStory.php
@@ -0,0 +1,35 @@
+ 'apple', 'name' => 'Apple'],
+ ['id' => 'banana', 'name' => 'Banana'],
+ ['id' => 'cherry', 'name' => 'Cherry'],
+ ['id' => 'coconut', 'name' => 'Coconut'],
+ ['id' => 'grape', 'name' => 'Grape'],
+ ['id' => 'kiwi', 'name' => 'Kiwi'],
+ ['id' => 'lemon', 'name' => 'Lemon'],
+ ['id' => 'mango', 'name' => 'Mango'],
+ ['id' => 'orange', 'name' => 'Orange'],
+ ['id' => 'papaya', 'name' => 'Papaya'],
+ ['id' => 'peach', 'name' => 'Peach'],
+ ['id' => 'pineapple', 'name' => 'Pineapple'],
+ ['id' => 'pear', 'name' => 'Pear'],
+ ['id' => 'pomegranate', 'name' => 'Pomegranate'],
+ ['id' => 'pomelo', 'name' => 'Pomelo'],
+ ['id' => 'raspberry', 'name' => 'Raspberry'],
+ ['id' => 'strawberry', 'name' => 'Strawberry'],
+ ['id' => 'watermelon', 'name' => 'Watermelon'],
+ ]);
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithAliasedLiveProps.php b/apps/e2e/src/Twig/Components/LiveComponentWithAliasedLiveProps.php
new file mode 100644
index 00000000000..5e5d62b302d
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithAliasedLiveProps.php
@@ -0,0 +1,25 @@
+withUrl(new UrlMapping(as: 'cat'));
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithDto.php b/apps/e2e/src/Twig/Components/LiveComponentWithDto.php
new file mode 100644
index 00000000000..7c187c9c91d
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithDto.php
@@ -0,0 +1,29 @@
+address = Address::create(
+ country: 'France',
+ city: 'Lyon',
+ );
+
+ return $data;
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndCustomHydrationMethods.php b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndCustomHydrationMethods.php
new file mode 100644
index 00000000000..9d5fee1237e
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndCustomHydrationMethods.php
@@ -0,0 +1,51 @@
+address = Address::create(
+ country: 'France',
+ city: 'Lyon',
+ );
+ }
+
+ public function dehydrateAddress(Address|null $address): array|null
+ {
+ if (null === $address) {
+ return null;
+ }
+
+ return [
+ 'x-country' => $address->country,
+ 'x-city' => $address->city
+ ];
+ }
+
+ public function hydrateAddress(array|null $data): Address
+ {
+ $address = new Address();
+
+ if (null !== $data) {
+ $address->country = $data['x-country'];
+ $address->city = $data['x-city'];
+ }
+
+ return $address;
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndHydrationExtension.php b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndHydrationExtension.php
new file mode 100644
index 00000000000..fb833886c75
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndHydrationExtension.php
@@ -0,0 +1,27 @@
+point = Point::create(
+ x: 69.420,
+ y: -1.337,
+ );
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndSerializer.php b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndSerializer.php
new file mode 100644
index 00000000000..9fa5a78bf2a
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithDtoAndSerializer.php
@@ -0,0 +1,27 @@
+address = Address::create(
+ country: 'France',
+ city: 'Lyon',
+ );
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveComponentWithDtoCollection.php b/apps/e2e/src/Twig/Components/LiveComponentWithDtoCollection.php
new file mode 100644
index 00000000000..0f473f8468a
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveComponentWithDtoCollection.php
@@ -0,0 +1,53 @@
+canAddAddress()) {
+ return;
+ }
+
+ match(count($this->addresses)) {
+ 0 => $this->addresses[] = Address::create(
+ country: 'France',
+ city: 'Lyon',
+ ),
+ 1 => $this->addresses[] = Address::create(
+ country: 'South Korea',
+ city: 'Seoul',
+ ),
+ default => null,
+ };
+ }
+
+ #[LiveAction]
+ public function reset(): void
+ {
+ $this->addresses = [];
+ }
+
+ public function canAddAddress(): bool
+ {
+ return count($this->addresses) < 2;
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveCounter.php b/apps/e2e/src/Twig/Components/LiveCounter.php
new file mode 100644
index 00000000000..241ab6d43c5
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveCounter.php
@@ -0,0 +1,29 @@
+value -= 1;
+ }
+
+ #[LiveAction]
+ public function increment(): void
+ {
+ $this->value += 1;
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveExamplesSearch.php b/apps/e2e/src/Twig/Components/LiveExamplesSearch.php
new file mode 100644
index 00000000000..ebbbb295f71
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveExamplesSearch.php
@@ -0,0 +1,34 @@
+exampleRepository->findAllGroupedByPackage($this->query);
+ }
+
+ #[LiveAction]
+ public function clearQuery(): void
+ {
+ $this->query = '';
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveFruitsPagination.php b/apps/e2e/src/Twig/Components/LiveFruitsPagination.php
new file mode 100644
index 00000000000..8a850290612
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveFruitsPagination.php
@@ -0,0 +1,58 @@
+page < 1 ? 1 : $this->page;
+
+ return $this->fruitRepository->paginate(page: $page, perPage: 5);
+ }
+
+ public function hasPreviousPage(): bool
+ {
+ return $this->page > 1;
+ }
+
+ public function hasNextPage(): bool
+ {
+ // not very efficient, but good enough for this example
+ return \count($this->fruitRepository->paginate(page: $this->page + 1, perPage: 8)) > 0;
+ }
+
+ #[LiveAction]
+ public function goToPreviousPage(): void
+ {
+ if ($this->hasPreviousPage()) {
+ $this->page--;
+ }
+ }
+
+ #[LiveAction]
+ public function goToNextPage(): void
+ {
+ if ($this->hasNextPage()) {
+ $this->page++;
+ }
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveItemList.php b/apps/e2e/src/Twig/Components/LiveItemList.php
new file mode 100644
index 00000000000..bdc1799f297
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveItemList.php
@@ -0,0 +1,41 @@
+items[] = '';
+ }
+
+ #[LiveAction]
+ public function deleteItems(): void
+ {
+ $this->items = [];
+ }
+
+ #[LiveAction]
+ public function deleteItem(#[LiveArg] int $key): void
+ {
+ unset($this->items[$key]);
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/LiveRegistrationForm.php b/apps/e2e/src/Twig/Components/LiveRegistrationForm.php
new file mode 100644
index 00000000000..c4c88ecf653
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/LiveRegistrationForm.php
@@ -0,0 +1,61 @@
+getForm()->isSubmitted() && !$this->getForm()->isValid();
+ }
+
+ #[LiveAction]
+ public function saveRegistration(): void
+ {
+ $this->submitForm();
+ $this->isSuccessful = true;
+ }
+
+ protected function instantiateForm(): FormInterface
+ {
+ return $this->formFactory->createBuilder()
+ ->add('email', EmailType::class, [
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Email(),
+ ],
+ ])
+ ->add('password', PasswordType::class, [
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Length(['min' => 8]),
+ ],
+ // prevent password from being emptied on re-render
+ 'always_empty' => false,
+ ])
+ ->getForm();
+ }
+}
diff --git a/apps/e2e/src/Twig/Components/ProductionForm.php b/apps/e2e/src/Twig/Components/ProductionForm.php
new file mode 100644
index 00000000000..3c67094744f
--- /dev/null
+++ b/apps/e2e/src/Twig/Components/ProductionForm.php
@@ -0,0 +1,43 @@
+createForm(ProductionType::class, $this->initialFormData ?? new ProductionDto());
+ }
+
+ #[LiveListener('movie-selected')]
+ public function onMovieSelected(#[LiveArg] string $title): void
+ {
+ $this->formValues['title'] = $title;
+ }
+
+ #[LiveListener('videogame-selected')]
+ public function onVideogameSelected(#[LiveArg] string $title): void
+ {
+ $this->formValues['title'] = $title;
+ }
+}
diff --git a/apps/e2e/src/Twig/Extension/AppExtension.php b/apps/e2e/src/Twig/Extension/AppExtension.php
new file mode 100644
index 00000000000..82437bb1d01
--- /dev/null
+++ b/apps/e2e/src/Twig/Extension/AppExtension.php
@@ -0,0 +1,16 @@
+'.print_r($value, true).'';
+ }, ['is_safe' => ['html']]);
+ }
+}
\ No newline at end of file
diff --git a/apps/e2e/src/UxPackage.php b/apps/e2e/src/UxPackage.php
index 8e53867765e..14325d19856 100644
--- a/apps/e2e/src/UxPackage.php
+++ b/apps/e2e/src/UxPackage.php
@@ -17,6 +17,7 @@ enum UxPackage: string
case ChartJs = 'UX Chart';
case Cropperjs = 'UX Cropperjs';
case Icons = 'UX Icons';
+ case LiveComponent = 'UX LiveComponent';
//case LazyImage = 'UX LazyImage'; // deprecated/removed
case Map = 'UX Map';
case Notify = 'UX Notify';
@@ -28,7 +29,6 @@ enum UxPackage: string
// case Toolkit; // not subject to E2E
case Translator = 'UX Translator';
case Turbo = 'UX Turbo';
- case TwigComponent = 'UX TwigComponent';
// case Typed; // deprecated
case Vue = 'UX Vue';
@@ -39,6 +39,7 @@ public function getDocumentationUrl(): string
self::ChartJs => 'https://ux.symfony.com/chartjs',
self::Cropperjs => 'https://ux.symfony.com/cropperjs',
self::Icons => 'https://ux.symfony.com/icons',
+ self::LiveComponent => 'https://ux.symfony.com/live-component',
self::Map => 'https://ux.symfony.com/map',
self::Notify => 'https://ux.symfony.com/notify',
self::React => 'https://ux.symfony.com/react',
@@ -46,7 +47,6 @@ public function getDocumentationUrl(): string
self::Svelte => 'https://ux.symfony.com/svelte',
self::Translator => 'https://ux.symfony.com/translator',
self::Turbo => 'https://ux.symfony.com/turbo',
- self::TwigComponent => 'https://ux.symfony.com/twig-component',
self::Vue => 'https://ux.symfony.com/vue',
};
}
diff --git a/apps/e2e/symfony.lock b/apps/e2e/symfony.lock
index fc7be91565d..e84905f5a25 100644
--- a/apps/e2e/symfony.lock
+++ b/apps/e2e/symfony.lock
@@ -452,5 +452,18 @@
},
"twig/extra-bundle": {
"version": "v3.21.0"
+ },
+ "zenstruck/foundry": {
+ "version": "2.8",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.7",
+ "ref": "7fc98f546dfeaa83cc2110634f8ff078d070b965"
+ },
+ "files": [
+ "config/packages/zenstruck_foundry.yaml",
+ "src/Story/AppStory.php"
+ ]
}
}
diff --git a/apps/e2e/templates/base.html.twig b/apps/e2e/templates/base.html.twig
index 001a22a5580..ba9bef57d0c 100644
--- a/apps/e2e/templates/base.html.twig
+++ b/apps/e2e/templates/base.html.twig
@@ -1,5 +1,5 @@
-
+
{% block title %}Symfony UX's E2E App{% endblock %}
@@ -8,6 +8,10 @@
{% endblock %}
{% block javascripts %}
+
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
diff --git a/apps/e2e/templates/components/LiveComponentWithAliasedLiveProps.html.twig b/apps/e2e/templates/components/LiveComponentWithAliasedLiveProps.html.twig
new file mode 100644
index 00000000000..f76b32d56b8
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithAliasedLiveProps.html.twig
@@ -0,0 +1,12 @@
+
diff --git a/apps/e2e/templates/components/LiveComponentWithDto.html.twig b/apps/e2e/templates/components/LiveComponentWithDto.html.twig
new file mode 100644
index 00000000000..466f14c70b9
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithDto.html.twig
@@ -0,0 +1,17 @@
+
+
+
+
+
+
Address (print_r)
+
+
diff --git a/apps/e2e/templates/components/LiveComponentWithDtoAndCustomHydrationMethods.html.twig b/apps/e2e/templates/components/LiveComponentWithDtoAndCustomHydrationMethods.html.twig
new file mode 100644
index 00000000000..555cf183574
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithDtoAndCustomHydrationMethods.html.twig
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Address (print_r)
+
+
diff --git a/apps/e2e/templates/components/LiveComponentWithDtoAndHydrationExtension.html.twig b/apps/e2e/templates/components/LiveComponentWithDtoAndHydrationExtension.html.twig
new file mode 100644
index 00000000000..514c1886b92
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithDtoAndHydrationExtension.html.twig
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Point (print_r)
+
+
diff --git a/apps/e2e/templates/components/LiveComponentWithDtoAndSerializer.html.twig b/apps/e2e/templates/components/LiveComponentWithDtoAndSerializer.html.twig
new file mode 100644
index 00000000000..555cf183574
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithDtoAndSerializer.html.twig
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Address (print_r)
+
+
diff --git a/apps/e2e/templates/components/LiveComponentWithDtoCollection.html.twig b/apps/e2e/templates/components/LiveComponentWithDtoCollection.html.twig
new file mode 100644
index 00000000000..2f59055c544
--- /dev/null
+++ b/apps/e2e/templates/components/LiveComponentWithDtoCollection.html.twig
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Addresses (print_r)
+
+
diff --git a/apps/e2e/templates/components/LiveCounter.html.twig b/apps/e2e/templates/components/LiveCounter.html.twig
new file mode 100644
index 00000000000..ac741f22a02
--- /dev/null
+++ b/apps/e2e/templates/components/LiveCounter.html.twig
@@ -0,0 +1,19 @@
+
+
+
+
+ {{ value }}
+
+
+
+
diff --git a/apps/e2e/templates/components/LiveExamplesSearch.html.twig b/apps/e2e/templates/components/LiveExamplesSearch.html.twig
new file mode 100644
index 00000000000..ff26ba0387d
--- /dev/null
+++ b/apps/e2e/templates/components/LiveExamplesSearch.html.twig
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for package, examples in computed.examplesGroupedByPackage %}
+ {% set package = enum('App\\UxPackage').from(package) %}
+
{{ package.value }} 📖
+
+ {% for example in examples %}
+
+
+
+
{{ example.name }}
+
{{ example.description }}
+
See example
+
+
+
+ {% endfor %}
+
+ {% else %}
+ No results found.
+ {% endfor %}
+
+
diff --git a/apps/e2e/templates/components/LiveFruitsPagination.html.twig b/apps/e2e/templates/components/LiveFruitsPagination.html.twig
new file mode 100644
index 00000000000..5db302a627c
--- /dev/null
+++ b/apps/e2e/templates/components/LiveFruitsPagination.html.twig
@@ -0,0 +1,22 @@
+
+
Page {{ page }}
+
+ {% for fruit in this.fruits %}
+ - {{ fruit.name }}
+ {% endfor %}
+
+
+
diff --git a/apps/e2e/templates/components/LiveItemList.html.twig b/apps/e2e/templates/components/LiveItemList.html.twig
new file mode 100644
index 00000000000..b8a758e3b77
--- /dev/null
+++ b/apps/e2e/templates/components/LiveItemList.html.twig
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+ {% if items|length > 0 %}
+
+ {% else %}
+
No items.
+ {% endif %}
+
diff --git a/apps/e2e/templates/components/LiveRegistrationForm.html.twig b/apps/e2e/templates/components/LiveRegistrationForm.html.twig
new file mode 100644
index 00000000000..5e2f1da01c6
--- /dev/null
+++ b/apps/e2e/templates/components/LiveRegistrationForm.html.twig
@@ -0,0 +1,27 @@
+{% form_theme form 'bootstrap_5_layout.html.twig' %}
+
+ {% if isSuccessful %}
+
+ Registration successful!
+
+ {% else %}
+ {{ form_start(form, {
+ attr: {
+ novalidate: true,
+ 'data-action': 'live#action:prevent',
+ 'data-live-action-param': 'saveRegistration',
+ }
+ }) }}
+ {{ form_row(form.email) }}
+ {{ form_row(form.password) }}
+
+
+
+ {{ form_rest(form) }}
+ {{ form_end(form) }}
+ {% endif %}
+
diff --git a/apps/e2e/templates/components/ProductionForm.html.twig b/apps/e2e/templates/components/ProductionForm.html.twig
new file mode 100644
index 00000000000..fb37b60d318
--- /dev/null
+++ b/apps/e2e/templates/components/ProductionForm.html.twig
@@ -0,0 +1,34 @@
+{% form_theme form 'bootstrap_5_layout.html.twig' %}
+
+ {{ form_start(form) }}
+
+ {{ form_label(form.type) }}
+ {{ form_widget(form.type) }}
+ {{ form_errors(form.type) }}
+
+
+ {% if form.movieSearch is defined %}
+
+ {{ form_label(form.movieSearch) }}
+ {{ form_widget(form.movieSearch) }}
+ {{ form_errors(form.movieSearch) }}
+
+ {% endif %}
+
+ {% if form.videogameSearch is defined %}
+
+ {{ form_label(form.videogameSearch) }}
+ {{ form_widget(form.videogameSearch) }}
+ {{ form_errors(form.videogameSearch) }}
+
+ {% endif %}
+
+
+ {{ form_label(form.title) }}
+ {{ form_widget(form.title) }}
+ {{ form_errors(form.title) }}
+
+
+
+ {{ form_end(form) }}
+
diff --git a/apps/e2e/templates/home.html.twig b/apps/e2e/templates/home.html.twig
index a5f188ab23c..3e0040a3086 100644
--- a/apps/e2e/templates/home.html.twig
+++ b/apps/e2e/templates/home.html.twig
@@ -13,22 +13,6 @@
- {% for package, examples in examples_by_package %}
- {% set package = enum('App\\UxPackage').from(package) %}
-
{{ package.value }} 📖
-
- {% for example in examples %}
-
-
-
-
{{ example.name }}
-
{{ example.description }}
-
See example
-
-
-
- {% endfor %}
-
- {% endfor %}
+
{% endblock %}
diff --git a/apps/e2e/templates/test/autocomplete_dynamic_form.html.twig b/apps/e2e/templates/test/autocomplete_dynamic_form.html.twig
new file mode 100644
index 00000000000..f262bd6b044
--- /dev/null
+++ b/apps/e2e/templates/test/autocomplete_dynamic_form.html.twig
@@ -0,0 +1,12 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Autocomplete Dynamic Form Test{% endblock %}
+
+{% block main %}
+
+
Autocomplete with Dynamic Forms
+
This test page demonstrates dynamic autocomplete fields within a LiveComponent form.
+
+
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_autocomplete/custom_controller.html.twig b/apps/e2e/templates/ux_autocomplete/custom_controller.html.twig
new file mode 100644
index 00000000000..7793a3d8d94
--- /dev/null
+++ b/apps/e2e/templates/ux_autocomplete/custom_controller.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+ {{ component('ProductionForm') }}
+{% endblock %}
diff --git a/apps/e2e/templates/ux_autocomplete/index.html.twig b/apps/e2e/templates/ux_autocomplete/index.html.twig
deleted file mode 100644
index 78c01e96007..00000000000
--- a/apps/e2e/templates/ux_autocomplete/index.html.twig
+++ /dev/null
@@ -1,3 +0,0 @@
-{% extends 'example.html.twig' %}
-
-{% block example %}{% endblock %}
diff --git a/apps/e2e/templates/ux_autocomplete/with_ajax.html.twig b/apps/e2e/templates/ux_autocomplete/with_ajax.html.twig
new file mode 100644
index 00000000000..f99295f8d25
--- /dev/null
+++ b/apps/e2e/templates/ux_autocomplete/with_ajax.html.twig
@@ -0,0 +1,7 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+ {{ form_start(form) }}
+ {{ form_row(form.favorite_fruit) }}
+ {{ form_end(form) }}
+{% endblock %}
diff --git a/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig b/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig
new file mode 100644
index 00000000000..f99295f8d25
--- /dev/null
+++ b/apps/e2e/templates/ux_autocomplete/without_ajax.html.twig
@@ -0,0 +1,7 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+ {{ form_start(form) }}
+ {{ form_row(form.favorite_fruit) }}
+ {{ form_end(form) }}
+{% endblock %}
diff --git a/apps/e2e/templates/ux_chartjs/index.html.twig b/apps/e2e/templates/ux_chartjs/index.html.twig
index 78c01e96007..40c8655d3f7 100644
--- a/apps/e2e/templates/ux_chartjs/index.html.twig
+++ b/apps/e2e/templates/ux_chartjs/index.html.twig
@@ -1,3 +1,14 @@
{% extends 'example.html.twig' %}
-{% block example %}{% endblock %}
+{% block example %}
+
+ {{ render_chart(chart, {'id': 'test-chart'}) }}
+
+
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_cropperjs/crop.html.twig b/apps/e2e/templates/ux_cropperjs/crop.html.twig
new file mode 100644
index 00000000000..057c6a176f3
--- /dev/null
+++ b/apps/e2e/templates/ux_cropperjs/crop.html.twig
@@ -0,0 +1,26 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+
+ {% if croppedImageData %}
+
+
Image cropped successfully!
+
Here is your cropped image:
+

+
+ {% endif %}
+
+ {{ form_start(form, { attr: { 'data-turbo': 'false' } }) }}
+ {{ form_row(form.crop) }}
+
+
+
+ {{ form_end(form) }}
+
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_cropperjs/index.html.twig b/apps/e2e/templates/ux_cropperjs/index.html.twig
deleted file mode 100644
index 78c01e96007..00000000000
--- a/apps/e2e/templates/ux_cropperjs/index.html.twig
+++ /dev/null
@@ -1,3 +0,0 @@
-{% extends 'example.html.twig' %}
-
-{% block example %}{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/counter.html.twig b/apps/e2e/templates/ux_live_component/counter.html.twig
new file mode 100644
index 00000000000..b86b3ab7e09
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/counter.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/fruits.html.twig b/apps/e2e/templates/ux_live_component/fruits.html.twig
new file mode 100644
index 00000000000..8e462a75715
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/fruits.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/index.html.twig b/apps/e2e/templates/ux_live_component/index.html.twig
deleted file mode 100644
index 78c01e96007..00000000000
--- a/apps/e2e/templates/ux_live_component/index.html.twig
+++ /dev/null
@@ -1,3 +0,0 @@
-{% extends 'example.html.twig' %}
-
-{% block example %}{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/item_list.html.twig b/apps/e2e/templates/ux_live_component/item_list.html.twig
new file mode 100644
index 00000000000..6a1f90fee35
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/item_list.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/registration_form.html.twig b/apps/e2e/templates/ux_live_component/registration_form.html.twig
new file mode 100644
index 00000000000..65ad46ababc
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/registration_form.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_aliased_live_props.html.twig b/apps/e2e/templates/ux_live_component/with_aliased_live_props.html.twig
new file mode 100644
index 00000000000..51e5d7bd49c
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_aliased_live_props.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_dto.html.twig b/apps/e2e/templates/ux_live_component/with_dto.html.twig
new file mode 100644
index 00000000000..152c417cac2
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_dto.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_dto_and_custom_hydration_methods.html.twig b/apps/e2e/templates/ux_live_component/with_dto_and_custom_hydration_methods.html.twig
new file mode 100644
index 00000000000..4402fa18e47
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_dto_and_custom_hydration_methods.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_dto_and_hydration_extension.html.twig b/apps/e2e/templates/ux_live_component/with_dto_and_hydration_extension.html.twig
new file mode 100644
index 00000000000..49034186e18
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_dto_and_hydration_extension.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_dto_and_serializer.html.twig b/apps/e2e/templates/ux_live_component/with_dto_and_serializer.html.twig
new file mode 100644
index 00000000000..e6e02942def
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_dto_and_serializer.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_live_component/with_dto_collection.html.twig b/apps/e2e/templates/ux_live_component/with_dto_collection.html.twig
new file mode 100644
index 00000000000..d385c205864
--- /dev/null
+++ b/apps/e2e/templates/ux_live_component/with_dto_collection.html.twig
@@ -0,0 +1,5 @@
+{% extends 'example.html.twig' %}
+
+{% block example %}
+
+{% endblock %}
diff --git a/apps/e2e/templates/ux_translator/basic.html.twig b/apps/e2e/templates/ux_translator/basic.html.twig
index 476d2f335b6..c07aefc9572 100644
--- a/apps/e2e/templates/ux_translator/basic.html.twig
+++ b/apps/e2e/templates/ux_translator/basic.html.twig
@@ -11,15 +11,14 @@
{% block javascripts %}
{{ parent() }}