Reputation: 145
long question short, after researching about this a lot and finding quite some information on how to extend existing field types, or inherit from them, or change some things in the backend, but absolutely none for the actual rendering in the frontend, I'm coming here to ask the question.
Short explanation to the "issue" at hand: I need an EntityType field (ChoiceType - HTML Select) to use my own filtering logic and to dynamically pull results from an ajax call, instantly replacing the options listed in the dropdown.
Current code (works): in FormType.php
//in buildForm
{
$builder->add('trainer', EntityType::class, [
'class' => Trainer::class,
'choices' => $training->trainer_list ?? [],
'label' => 'seminar.trainer.form.trainer.label',
'placeholder' => 'form.trainer.placeholder',
'required' => false,
'attr' => ['class' => 'trainer2select'] // has no outcome whatsoever?!
])
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']);
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
$trainer = $this->em->getRepository(Trainer::class)->find($data['trainer']);
$form->add('trainer', EntityType::class, [
'class' => Trainer::class,
'data' => $trainer,
'label' => 'seminar.trainer.form.trainer.label',
'placeholder' => 'form.trainer.placeholder',
'required' => false,
]);
}
And in twig:
{% if field == 'trainer' %}
{{ form_row(attribute(form, field), {'id': 'trainer'}) }}
{% else %}
{{ form_row(attribute(form, field)) }}
{% endif %}
{% block javascripts %}
<script>
var lastTime;
var timeoutEvents = [];
$(document).ready(() => {
function trainer_changed() {
let input = event.target;
lastTime = Date.now();
timeoutEvents.push(setTimeout(() => {
if (Date.now() - lastTime < 150)
return;
jQuery.ajax({
url: '{{ path('trainer_select_ajax') }}',
type: 'GET',
data: {
search: input.value,
start: {{ seminar.event.start.date | date('Y-m-d') }},
end: {{ seminar.event.end.date | date('Y-m-d') }}
},
success: function (trainers) {
let trainer = $('#trainer');
trainer.get(0).options.length = 1; // reset all options, except for the default
trainers.forEach(tr => {
trainer.append(new Option(tr.text, tr.id));
});
let search = $($("input.select2-search__field").get(1));
if (search.get(0)) {
search.get(0).oninput = null; // detach our event handler so we don't loop
search.trigger('input'); // rebuild the dropdown choices
search.get(0).oninput = trainer_changed; // reattach our event handler
}
}
});
lastTime = Date.now();
timeoutEvents.forEach(e => {
clearTimeout(e);
});
}, 200));
}
function select_opened() {
let trainerinput = $('input.select2-search__field').get(1);
if (trainerinput) {
trainerinput.oninput = trainer_changed;
}
}
$('#select2-trainer-container').click(select_opened);
});
</script>
{% endblock %}
So, apparently the EntityType Field is rendered using the select2 extension. I can obviously replace the functionality with javascript, but I'd like to just define my own 'AjaxEntityType' that form_widget renders as I want it to. Something I can use within multiple projects, without using some stupid hacks like providing a default class name and invoking javascript changing that rendering after page load globally. So... how do?
Resources I've checked, that have proven mostly worthless to what I want to achieve: https://symfony.com/doc/current/form/form_customization.html, https://symfony.com/doc/current/form/form_themes.html and plenty more.
Edit for clarification: What I'm looking for, optimally, is a minimalistic example of a custom FieldType always being rendered as <select id="FieldTypeWidget">
, on which will always be invoked $('#FieldTypeWidget').select2({ajax: {foo}, searchFunction: {bar}});
at https://github.com/tetranz/select2entity-bundle I can find an example of how to provide this functionality in a bundle, but is there an easier way just within my app?
Upvotes: 2
Views: 1954
Reputation: 39129
I would say that what you are looking at is what is explained in this Symfony documentation page
Here is their example modified a bit for your needs:
src/Form/Type/AjaxEntityType.php
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EntityType;
class AjaxEntityType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
/**
* This is the default of your field,
* add or remove based on your needs,
* your goal, is to only keep sensible defaults
* that you want on every single objects of this class
*/
'required' => false,
]);
}
public function getParent()
{
return EntityType::class;
}
}
Here is where the 'magic' happens:
When your class is called WhateverNameType, Symfony will just remove the Type part of it and normalize it (in simplified, lcfirst
it).
So WhateverNameType will end as whateverName.
Then, you just have to know that form elements are called, in the rendering form_widget to end up whit the proper named block: whateverName_widget
templates/form/fields.html.twig
{% use 'form_div_layout.html.twig' %}
{% block ajaxEntity_widget %}
{{ parent() }}
<script>
$('#{{ form.vars.id }}').select2({ajax: {foo}, searchFunction: {bar}});
</script>
{% endblock %}
Please also note that handy tip from the documentation page:
You can further customize the template used to render each children of the choice type. The block to override in that case is named "block name" + entry + "element name" (label, errors or widget) (e.g. to customize the labels of the children of the Shipping widget you'd need to define {% block shipping_entry_label %} ... {% endblock %}).
And also remember, as noted later still on the same page, that your form template override has to be properly registered:
config/packages/twig.yaml
twig:
form_themes:
- 'form/fields.html.twig'
# you might have this configuration already,
# for example, if you use bootstrap theming.
# If so, just copy the configured template path stated here
# in the 'use' statement of the file form/fields.html.twig
Then just use it:
$builder->add('trainer', AjaxEntityType::class, [ class => Trainer::class, ]);
Worthy to also read:
Upvotes: 2