Skip to content

Commit 90caae6

Browse files
author
Marcus Stöhr
committed
test(autocomplete): add e2e-example for custom autocomplete controllers in a dynamic form
1 parent 197d81f commit 90caae6

14 files changed

+386
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { getComponent } from '@symfony/ux-live-component';
3+
4+
export default class extends Controller {
5+
async connect() {
6+
this.component = await getComponent(this.element.closest('[data-controller*="live"]'));
7+
8+
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
9+
this.element.addEventListener('autocomplete:connect', this._onConnect.bind(this));
10+
}
11+
12+
disconnect() {
13+
this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
14+
this.element.removeEventListener('autocomplete:connect', this._onConnect.bind(this));
15+
}
16+
17+
_onPreConnect(event) {
18+
const options = event.detail.options;
19+
options.render = {
20+
...options.render,
21+
option: (item) => {
22+
return `<div data-test-id="autocomplete-option" data-title="${item.title || item.text}">${item.text}</div>`;
23+
},
24+
};
25+
}
26+
27+
_onConnect(event) {
28+
const tomSelect = event.detail.tomSelect;
29+
30+
tomSelect.on('item_add', (value, item) => {
31+
const title = item.getAttribute('data-title') || item.textContent;
32+
this.component.emit('movie-selected', { title });
33+
});
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { getComponent } from '@symfony/ux-live-component';
3+
4+
export default class extends Controller {
5+
async connect() {
6+
this.component = await getComponent(this.element.closest('[data-controller*="live"]'));
7+
8+
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
9+
this.element.addEventListener('autocomplete:connect', this._onConnect.bind(this));
10+
}
11+
12+
disconnect() {
13+
this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect.bind(this));
14+
this.element.removeEventListener('autocomplete:connect', this._onConnect.bind(this));
15+
}
16+
17+
_onPreConnect(event) {
18+
const options = event.detail.options;
19+
options.render = {
20+
...options.render,
21+
option: (item) => {
22+
return `<div data-test-id="autocomplete-option" data-title="${item.title || item.text}">${item.text}</div>`;
23+
},
24+
};
25+
}
26+
27+
_onConnect(event) {
28+
const tomSelect = event.detail.tomSelect;
29+
30+
tomSelect.on('item_add', (value, item) => {
31+
const title = item.getAttribute('data-title') || item.textContent;
32+
this.component.emit('videogame-selected', { title });
33+
});
34+
}
35+
}

apps/e2e/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"symfony/ux-typed": "^2.29.1",
6262
"symfony/ux-vue": "^2.29.1",
6363
"symfony/yaml": "6.4.*|7.3.*",
64+
"symfonycasts/dynamic-forms": "^0.2",
6465
"twig/extra-bundle": "^3.21",
6566
"twig/twig": "^3.21.1"
6667
},

apps/e2e/src/Controller/AutocompleteController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ public function index()
1919
['form' => $form->createView()]
2020
);
2121
}
22+
23+
#[Route('/custom-controller')]
24+
public function customController()
25+
{
26+
return $this->render('ux_autocomplete/custom_controller.html.twig');
27+
}
2228
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\JsonResponse;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\Routing\Attribute\Route;
10+
11+
#[Route('/test')]
12+
final class TestAutocompleteController extends AbstractController
13+
{
14+
#[Route('/autocomplete-dynamic-form', name: 'test_autocomplete_dynamic_form')]
15+
public function dynamicForm(): Response
16+
{
17+
return $this->render('test/autocomplete_dynamic_form.html.twig');
18+
}
19+
20+
#[Route('/autocomplete/movie', name: 'test_autocomplete_movie')]
21+
public function movieAutocomplete(Request $request): JsonResponse
22+
{
23+
$query = $request->query->get('query', '');
24+
25+
$movies = [
26+
['value' => 'movie_1', 'text' => 'The Matrix (1999)', 'title' => 'movie Movie #1'],
27+
['value' => 'movie_2', 'text' => 'Inception (2010)', 'title' => 'movie Movie #2'],
28+
['value' => 'movie_3', 'text' => 'The Dark Knight (2008)', 'title' => 'movie Movie #3'],
29+
['value' => 'movie_4', 'text' => 'Interstellar (2014)', 'title' => 'movie Movie #4'],
30+
['value' => 'movie_5', 'text' => 'Pulp Fiction (1994)', 'title' => 'movie Movie #5'],
31+
];
32+
33+
$results = array_filter($movies, function ($movie) use ($query) {
34+
return '' === $query || false !== stripos($movie['text'], $query);
35+
});
36+
37+
return $this->json([
38+
'results' => array_values($results),
39+
]);
40+
}
41+
42+
#[Route('/autocomplete/videogame', name: 'test_autocomplete_videogame')]
43+
public function videogameAutocomplete(Request $request): JsonResponse
44+
{
45+
$query = $request->query->get('query', '');
46+
47+
$games = [
48+
['value' => 'videogame_1', 'text' => 'Halo: Combat Evolved (2001)', 'title' => 'videogame Game #1'],
49+
['value' => 'videogame_2', 'text' => 'The Legend of Zelda (1986)', 'title' => 'videogame Game #2'],
50+
['value' => 'videogame_3', 'text' => 'Half-Life 2 (2004)', 'title' => 'videogame Game #3'],
51+
['value' => 'videogame_4', 'text' => 'Portal (2007)', 'title' => 'videogame Game #4'],
52+
['value' => 'videogame_5', 'text' => 'Mass Effect 2 (2010)', 'title' => 'videogame Game #5'],
53+
];
54+
55+
$results = array_filter($games, function ($game) use ($query) {
56+
return '' === $query || false !== stripos($game['text'], $query);
57+
});
58+
59+
return $this->json([
60+
'results' => array_values($results),
61+
]);
62+
}
63+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Form\Model;
4+
5+
class ProductionDto
6+
{
7+
public ?string $type = null;
8+
9+
public ?string $movieSearch = null;
10+
11+
public ?string $videogameSearch = null;
12+
13+
public ?string $title = null;
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Form\Type;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\TextType;
7+
use Symfony\Component\OptionsResolver\OptionsResolver;
8+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
9+
10+
class MovieAutocompleteType extends AbstractType
11+
{
12+
public function __construct(
13+
private UrlGeneratorInterface $urlGenerator
14+
) {
15+
}
16+
17+
public function configureOptions(OptionsResolver $resolver): void
18+
{
19+
$resolver->setDefaults([
20+
'autocomplete' => true,
21+
'autocomplete_url' => $this->urlGenerator->generate('test_autocomplete_movie'),
22+
'tom_select_options' => [
23+
'maxOptions' => null,
24+
],
25+
'attr' => [
26+
'data-test-id' => 'movie-autocomplete',
27+
'data-controller' => 'movie-autocomplete',
28+
],
29+
]);
30+
}
31+
32+
public function getParent(): string
33+
{
34+
return TextType::class;
35+
}
36+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Form\Type;
4+
5+
use App\Form\Model\ProductionDto;
6+
use Symfony\Component\Form\AbstractType;
7+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
8+
use Symfony\Component\Form\Extension\Core\Type\TextType;
9+
use Symfony\Component\Form\FormBuilderInterface;
10+
use Symfony\Component\OptionsResolver\OptionsResolver;
11+
use Symfonycasts\DynamicForms\DependentField;
12+
use Symfonycasts\DynamicForms\DynamicFormBuilder;
13+
14+
class ProductionType extends AbstractType
15+
{
16+
public function buildForm(FormBuilderInterface $builder, array $options): void
17+
{
18+
$builder = new DynamicFormBuilder($builder);
19+
20+
$builder
21+
->add('type', ChoiceType::class, [
22+
'choices' => [
23+
'Movie' => 'movie',
24+
'Videogame' => 'videogame',
25+
],
26+
'placeholder' => 'Select a type',
27+
'attr' => [
28+
'data-test-id' => 'production-type',
29+
],
30+
])
31+
->addDependent('movieSearch', ['type'], function (DependentField $field, ?string $type) {
32+
if ('movie' !== $type) {
33+
return;
34+
}
35+
36+
$field->add(MovieAutocompleteType::class, [
37+
'label' => 'Search Movies',
38+
'required' => false,
39+
]);
40+
})
41+
->addDependent('videogameSearch', ['type'], function (DependentField $field, ?string $type) {
42+
if ('videogame' !== $type) {
43+
return;
44+
}
45+
46+
$field->add(VideogameAutocompleteType::class, [
47+
'label' => 'Search Videogames',
48+
'required' => false,
49+
]);
50+
})
51+
->add('title', TextType::class, [
52+
'required' => false,
53+
'attr' => [
54+
'data-test-id' => 'production-title',
55+
],
56+
])
57+
;
58+
}
59+
60+
public function configureOptions(OptionsResolver $resolver): void
61+
{
62+
$resolver->setDefaults([
63+
'data_class' => ProductionDto::class,
64+
]);
65+
}
66+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Form\Type;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\TextType;
7+
use Symfony\Component\OptionsResolver\OptionsResolver;
8+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
9+
10+
class VideogameAutocompleteType extends AbstractType
11+
{
12+
public function __construct(
13+
private UrlGeneratorInterface $urlGenerator
14+
) {
15+
}
16+
17+
public function configureOptions(OptionsResolver $resolver): void
18+
{
19+
$resolver->setDefaults([
20+
'autocomplete' => true,
21+
'autocomplete_url' => $this->urlGenerator->generate('test_autocomplete_videogame'),
22+
'tom_select_options' => [
23+
'maxOptions' => null,
24+
],
25+
'attr' => [
26+
'data-test-id' => 'videogame-autocomplete',
27+
'data-controller' => 'videogame-autocomplete',
28+
],
29+
]);
30+
}
31+
32+
public function getParent(): string
33+
{
34+
return TextType::class;
35+
}
36+
}

apps/e2e/src/Repository/ExampleRepository.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function __construct()
2525
{
2626
$this->examples = [
2727
new Example(UxPackage::Autocomplete, 'Autocomplete (non-ajax)', 'An autocomplete component to enhance a simple choice field.', '/ux-autocomplete/non-ajax'),
28+
new Example(UxPackage::Autocomplete, 'Autocomplete (custom controller)', 'An autocomplete component with a custom Stimulus controller for AJAX results.', '/ux-autocomplete/custom-controller'),
2829
new Example(UxPackage::Map, 'Basic map (Leaflet)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=leaflet'),
2930
new Example(UxPackage::Map, 'Basic map (Google)', 'A basic map centered on Paris with zoom level 12', '/ux-map/basic?renderer=google'),
3031
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'),

0 commit comments

Comments
 (0)