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/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/composer.json b/apps/e2e/composer.json
index 4736d725fae..8a868cfc154 100644
--- a/apps/e2e/composer.json
+++ b/apps/e2e/composer.json
@@ -38,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",
diff --git a/apps/e2e/src/Controller/AutocompleteController.php b/apps/e2e/src/Controller/AutocompleteController.php
index bc530c60543..2e1f21d99c3 100644
--- a/apps/e2e/src/Controller/AutocompleteController.php
+++ b/apps/e2e/src/Controller/AutocompleteController.php
@@ -8,10 +8,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-autocomplete')]
+#[Route('/ux-autocomplete', name: 'app_ux_autocomplete_')]
final class AutocompleteController extends AbstractController
{
- #[Route('/without-ajax')]
+ #[Route('/without-ajax', name: 'without_ajax')]
public function withoutAjax(): Response
{
$formBuilder = $this->createFormBuilder();
@@ -47,7 +47,7 @@ public function withoutAjax(): Response
]);
}
- #[Route('/with-ajax')]
+ #[Route('/with-ajax', name: 'with_ajax')]
public function withAjax(): Response
{
$formBuilder = $this->createFormBuilder();
@@ -60,7 +60,7 @@ public function withAjax(): Response
]);
}
- #[Route('/custom-controller')]
+ #[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..5e1814a4f2e 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);
diff --git a/apps/e2e/src/Controller/CropperjsController.php b/apps/e2e/src/Controller/CropperjsController.php
index cf9363c4dbc..0d5ea357f2a 100644
--- a/apps/e2e/src/Controller/CropperjsController.php
+++ b/apps/e2e/src/Controller/CropperjsController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-cropperjs')]
+#[Route('/ux-cropperjs', name: 'app_ux_cropperjs_')]
final class CropperjsController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_cropperjs/index.html.twig', [
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
index 72168a638e8..9a12f7a6b7c 100644
--- a/apps/e2e/src/Controller/TestAutocompleteController.php
+++ b/apps/e2e/src/Controller/TestAutocompleteController.php
@@ -8,17 +8,17 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/test')]
+#[Route('/test-autocomplete', name: 'app_test_autocomplete_')]
final class TestAutocompleteController extends AbstractController
{
- #[Route('/autocomplete-dynamic-form', name: 'test_autocomplete_dynamic_form')]
+ #[Route('/dynamic-form', name: 'dynamic_form')]
public function dynamicForm(): Response
{
return $this->render('test/autocomplete_dynamic_form.html.twig');
}
- #[Route('/autocomplete/movie', name: 'test_autocomplete_movie')]
- public function movieAutocomplete(Request $request): JsonResponse
+ #[Route('/movie', name: 'movie')]
+ public function movie(Request $request): JsonResponse
{
$query = $request->query->get('query', '');
@@ -39,8 +39,8 @@ public function movieAutocomplete(Request $request): JsonResponse
]);
}
- #[Route('/autocomplete/videogame', name: 'test_autocomplete_videogame')]
- public function videogameAutocomplete(Request $request): JsonResponse
+ #[Route('/videogame', name: 'videogame')]
+ public function videogame(Request $request): JsonResponse
{
$query = $request->query->get('query', '');
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..ea845b253f4 100644
--- a/apps/e2e/src/Controller/TurboController.php
+++ b/apps/e2e/src/Controller/TurboController.php
@@ -6,10 +6,10 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-#[Route('/ux-turbo')]
+#[Route('/ux-turbo', name: 'app_ux_turbo_')]
final class TurboController extends AbstractController
{
- #[Route('/')]
+ #[Route('/', name: 'index')]
public function index(): Response
{
return $this->render('ux_turbo/index.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/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/Form/Type/MovieAutocompleteType.php b/apps/e2e/src/Form/Type/MovieAutocompleteType.php
index e3326948fde..b487bd79495 100644
--- a/apps/e2e/src/Form/Type/MovieAutocompleteType.php
+++ b/apps/e2e/src/Form/Type/MovieAutocompleteType.php
@@ -18,7 +18,7 @@ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'autocomplete' => true,
- 'autocomplete_url' => $this->urlGenerator->generate('test_autocomplete_movie'),
+ 'autocomplete_url' => $this->urlGenerator->generate('app_test_autocomplete_movie'),
'tom_select_options' => [
'maxOptions' => null,
],
diff --git a/apps/e2e/src/Form/Type/VideogameAutocompleteType.php b/apps/e2e/src/Form/Type/VideogameAutocompleteType.php
index 3d7a2cb445d..dcd7abb2f4b 100644
--- a/apps/e2e/src/Form/Type/VideogameAutocompleteType.php
+++ b/apps/e2e/src/Form/Type/VideogameAutocompleteType.php
@@ -18,7 +18,7 @@ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'autocomplete' => true,
- 'autocomplete_url' => $this->urlGenerator->generate('test_autocomplete_videogame'),
+ 'autocomplete_url' => $this->urlGenerator->generate('app_test_autocomplete_videogame'),
'tom_select_options' => [
'maxOptions' => null,
],
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 86e8b78b673..04b0d1cf0e2 100644
--- a/apps/e2e/src/Repository/ExampleRepository.php
+++ b/apps/e2e/src/Repository/ExampleRepository.php
@@ -24,38 +24,48 @@ class ExampleRepository
public function __construct()
{
$this->examples = [
- new Example(UxPackage::Autocomplete, 'Autocomplete (with AJAX)', 'An autocomplete form field, by fetching results with AJAX.', '/ux-autocomplete/with-ajax'),
- new Example(UxPackage::Autocomplete, 'Autocomplete (without AJAX)', 'An autocomplete form field, by using the choses from the choice type field.', '/ux-autocomplete/without-ajax'),
- new Example(UxPackage::Autocomplete, 'Autocomplete (custom controller)', 'An autocomplete form field, with a custom Stimulus controller for AJAX results.', '/ux-autocomplete/custom-controller'),
- 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::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::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'),
];
}
@@ -67,21 +77,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
index 0118efe9de4..6d7bf4f12f8 100644
--- a/apps/e2e/src/Repository/FruitRepository.php
+++ b/apps/e2e/src/Repository/FruitRepository.php
@@ -15,4 +15,18 @@ 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/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/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/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) %}
+