简体   繁体   中英

Symfony - Custom FieldType form_widget rendering

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?

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) (eg 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:

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM