Skip to content

Commit 70f894d

Browse files
committed
Tweaks
1 parent 5e00ba3 commit 70f894d

3 files changed

Lines changed: 99 additions & 28 deletions

File tree

src/Form/EventListener/CrudAutocompleteSubscriber.php

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace EasyCorp\Bundle\EasyAdminBundle\Form\EventListener;
44

5-
use Doctrine\ORM\Mapping\FieldMapping;
65
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
76
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
87
use Symfony\Component\Form\FormEvent;
@@ -38,39 +37,31 @@ public function preSetData(FormEvent $event): void
3837
$options['compound'] = false;
3938
$options['choices'] = is_iterable($data) ? $data : [$data];
4039

41-
// apply custom choice_label if autocomplete is customized so the selected item looks the same as the other entries.
42-
// note: we don't escape here because Twig already escapes the <option> content automatically;
43-
// the renderAsHtml flag controls how TomSelect renders the item (via data-ea-autocomplete-render-items-as-html).
40+
// strip EA-specific options before forwarding to EntityType
4441
$callback = $options['autocomplete_callback'] ?? null;
4542
$template = $options['autocomplete_template'] ?? null;
46-
$choiceLabel = $options['choice_label'] ?? null;
47-
48-
// only generate choice_label from template/callback if not already set by the user
49-
if (null === $choiceLabel) {
43+
unset($options['autocomplete_callback'], $options['autocomplete_template']);
44+
45+
// Resolve choice_label:
46+
// - if the user supplied one (via value_type_options.choice_label), keep it;
47+
// - otherwise derive one from autocomplete_template / autocomplete_callback so the
48+
// selected item matches the rendering of other entries in the dropdown;
49+
// - otherwise drop the option entirely so EntityType falls back to __toString().
50+
//
51+
// Note: we don't escape here because Twig already escapes the <option> content
52+
// automatically; the renderAsHtml flag controls how TomSelect renders the item
53+
// (via data-ea-autocomplete-render-items-as-html).
54+
if (null === ($options['choice_label'] ?? null)) {
5055
if (null !== $template) {
5156
$twig = $this->twig;
52-
$options['choice_label'] = static function ($entity) use ($twig, $template): string {
53-
return $twig->render($template, ['entity' => $entity]);
54-
};
57+
$options['choice_label'] = static fn ($entity): string => $twig->render($template, ['entity' => $entity]);
5558
} elseif (null !== $callback) {
56-
$options['choice_label'] = static function ($entity) use ($callback): string {
57-
return (string) $callback($entity);
58-
};
59+
$options['choice_label'] = static fn ($entity): string => (string) $callback($entity);
60+
} else {
61+
unset($options['choice_label']);
5962
}
6063
}
6164

62-
// remove custom options before passing to EntityType
63-
unset(
64-
$options['autocomplete_callback'],
65-
$options['autocomplete_template']
66-
);
67-
68-
// Don't pass null choice_label to EntityType - let it use __toString() default
69-
// (passing null explicitly behaves differently than omitting the option in Symfony < 8.1)
70-
if (null === ($options['choice_label'] ?? null)) {
71-
unset($options['choice_label']);
72-
}
73-
7465
$form->add('autocomplete', EntityType::class, $options);
7566
}
7667

src/Form/Type/CrudAutocompleteType.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Symfony\Component\Form\FormInterface;
1010
use Symfony\Component\Form\FormView;
1111
use Symfony\Component\OptionsResolver\OptionsResolver;
12+
use Symfony\Component\PropertyAccess\PropertyPath;
1213
use Twig\Environment;
1314

1415
/**
@@ -48,14 +49,17 @@ public function configureOptions(OptionsResolver $resolver): void
4849
// options for custom rendering of selected items (to match the rendering of the other entries in the dropdown)
4950
'autocomplete_callback' => null,
5051
'autocomplete_template' => null,
51-
// allow choice_label to customize how selected items are displayed
52+
// forwarded to the underlying EntityType so users can customize the
53+
// label of the selected items via setFormTypeOption('value_type_options', ...)
5254
'choice_label' => null,
5355
]);
5456

5557
$resolver->setRequired(['class']);
5658
$resolver->setAllowedTypes('autocomplete_callback', ['null', 'callable']);
5759
$resolver->setAllowedTypes('autocomplete_template', ['null', 'string']);
58-
$resolver->setAllowedTypes('choice_label', ['null', 'string', 'callable', 'bool']);
60+
// mirror Symfony's ChoiceType allowed types for choice_label so anything accepted by
61+
// EntityType is also accepted here (we only skip ChoiceLabel::class, which is marked as internal)
62+
$resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class]);
5963
}
6064

6165
public function getBlockPrefix(): string

tests/Unit/Filter/CrudAutocompleteTypeTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,80 @@ public function testSubmitEmptyMultipleData(): void
150150
$this->assertTrue($form->isSynchronized());
151151
$this->assertEquals(new ArrayCollection(), $form->getData());
152152
}
153+
154+
public function testCustomChoiceLabelIsPreserved(): void
155+
{
156+
$project = (new Project())->setId(123)->setName('Foo');
157+
158+
$this->entityManager
159+
->method('contains')
160+
->with($project)
161+
->willReturn(true);
162+
$this->repository
163+
->method('findBy')
164+
->willReturn([$project]);
165+
$this->classMetadata
166+
->method('getIdentifierValues')
167+
->with($project)
168+
->willReturn(['id' => $project->getId()]);
169+
170+
$form = $this->factory->create(CrudAutocompleteType::class, null, [
171+
'class' => Project::class,
172+
'choice_label' => static fn (Project $p): string => sprintf('[%d] %s', $p->getId(),
173+
$p->getName()),
174+
]);
175+
$form->submit(['autocomplete' => '123']);
176+
177+
$this->assertTrue($form->isSynchronized());
178+
179+
$view = $form->createView();
180+
181+
// the user-supplied callable is used to build the option label,
182+
// overriding the default Project::__toString() output
183+
$this->assertEquals(
184+
['123' => new ChoiceView($project, 123, '[123] Foo')],
185+
$view->children['autocomplete']->vars['choices'],
186+
);
187+
}
188+
189+
public function testAutocompleteCallbackStillWinsWhenChoiceLabelIsNotSet(): void
190+
{
191+
$project = (new Project())->setId(123)->setName('Foo');
192+
193+
$this->entityManager
194+
->method('contains')
195+
->with($project)
196+
->willReturn(true);
197+
$this->repository
198+
->method('findBy')
199+
->willReturn([$project]);
200+
$this->classMetadata
201+
->method('getIdentifierValues')
202+
->with($project)
203+
->willReturn(['id' => $project->getId()]);
204+
205+
$form = $this->factory->create(CrudAutocompleteType::class, null, [
206+
'class' => Project::class,
207+
'autocomplete_callback' => static fn (Project $p): string => 'cb:'.$p->getName(),
208+
]);
209+
$form->submit(['autocomplete' => '123']);
210+
211+
$view = $form->createView();
212+
213+
$this->assertEquals(
214+
['123' => new ChoiceView($project, 123, 'cb:Foo')],
215+
$view->children['autocomplete']->vars['choices'],
216+
);
217+
}
218+
219+
public function testChoiceLabelOptionRejectsInvalidType(): void
220+
{
221+
222+
$this->expectException(\Symfony\Component\OptionsResolver\Exception\InvalidOptionsException::class);
223+
224+
$this->factory->create(CrudAutocompleteType::class, null, [
225+
'class' => Project::class,
226+
'choice_label' => 42, // int is not in the allowed types
227+
]);
228+
}
153229
}

0 commit comments

Comments
 (0)