Embed nested collections form in Symfony 6 - forms

I want to embed nested collections on three levels, for the second level it works but not the last one i can't get the prototype on the last level.
I work with Symfony 6 and i use stimulus.
class FormType1 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field1')
->add('formType2', CollectionType::class, [
'entry_type' => FormType2::class,
'entry_options' => ['label' => false],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData1::class,
]);
}
}
class FormType2 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field2')
->add('formType3', CollectionType::class, [
'entry_type' => FormType3::class,
'entry_options' => ['label' => false],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData2::class,
]);
}
}
class FormType3 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field3')
->add('field4');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FormData3::class,
]);
}
}
Here is the prototype:
<div {{ stimulus_controller('form-collection') }}
data-form-collection-index-value="{{ form.formType3|length > 0 ? form.formType3|last.vars.name + 1 : 0 }}"
data-form-collection-prototype-value="{{ form_widget(form.formType3.vars.prototype)|e('html_attr') }}" <<============
>
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
<button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button>
</div>
In Stimulus Controller form-collection i get the prototype in this way
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
The prototypeValue It return an empty string

Related

Custom Choice type with parameters

I make my custom ChoiceType
class CountryType extends ChoiceType
{
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'choices' => $this->locationService->getCountries(),
]);
}
}
class StateType extends ChoiceType
{
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$choices = isset($countryId)? $this->locationService->getStatesByCountryId($countryId):[]; <---
$resolver->setDefaults([
'choices' => $choices,
]);
}
}
class FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('country', CountryType::class, [
'label' => 'Country',
'required' => false,
])->add('state', StateType::class, [
'label' => 'State',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => FormRequest::class,
'validation_groups' => [],
]);
}
}
But for StateType optionally (when I edit entity) I need $countryId how I can put it? Maybe with options or choice_loader?
I also tried to crank out such an idea, but as I understand it, this is not possible
{{ form_widget(form.state, {
'countryId': form.country.vars.value,
}) }}

How to retrive data from non-mapped field in embeded form

I would like to know if it's possible to get the data from a non-mapped field in an embeded form.
Here is the main form :
class PlayedLifeScoreType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//->add('nom')
//->add('prenom')
// NOTE: Use form collection to allow multiple `played` forms per JoueurType
->add('playeds', CollectionType::class, [
'entry_type' => PlayedLifeType::class,
'label' => false,
])
->add('submit', SubmitType::class, [
'attr' => ['class' => 'save'],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Partie::class,
]);
}
}
And the embedded one :
class PlayedLifeType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
/*->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$form->add('joueur', null, array(
'data' => $event->getData() ?: options['joueur']
))*/
//->add('joueur')
->add('joueur', TextType::class, [
'label' => false,
'disabled' => 'true',
'attr' => ['class' => 'tinymce'],
])
->add('max')
->add('score')
->add('round', IntegerType::class,[
'mapped' => false,
'label' => 'Round',
])
;
//});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Played::class,
'joueur' => null
]);
}
}
And want to get the data from "round". I tried like this but doesn't work :
$r = $mainForm->get('playeds')->get("round")->getData();
I get this error :
Child "round" does not exist.
Any idea ?
field "playeds" is a CollectionType.
So, for each entry, there is a 'round' value
To access this, you should do something like:
foreach ($mainForm->get('playeds') as $played) {
//you can access round here with $played->get('round')->getData()
//Or the Played object with $played->getData()
}

Options to subform

How i can give form's options to a subform ?
In the example below, i have the option "special" declared.
I want to access this option "special" in my subform.
My main form :
class DemandeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', TextType::class, []);
$builder->add('service_agent', ServiceType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}
And my subform :
class ServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($options['special']);
$builder->add('service', TextType::class, []);
$builder->add('agent', TextType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
}
I answer my question.
I must declare option "special" in subform.
And this field's option is accessible in the main form.
Like that :
class DemandeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', TextType::class, []);
$builder->add('service_agent', ServiceType::class, [
'mapped' => false,
'special' => $options['special']
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}
And in my subform :
class ServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($options['special']); // It's OK :)
$builder->add('service', TextType::class, []);
$builder->add('agent', TextType::class, [
'mapped' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'special' => true
]);
}

Submit array of tag names and convert to entities

I want to do such a trick: submit tag names as an array and then convert them to entities. Submitted data:
{"title":"Title","description":"Desc", "tags": ["first", "second"]}
Form:
class ImageType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('imageFile')
->add('tags', CollectionType::class, [
'entry_type' => ImageTagType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Image::class,
'csrf_protection' => false,
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_image';
}
}
Form for image tag:
class ImageTagType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\ImageTag'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_imagetag';
}
}
The entities are connected with many-to-many. One image can have many tags and one tag can have many images. I am building an API and want to simplify tags submission. What should I do?
Use DataTransformer. Example:
....
$builder->get('YOUR_FILED')
->addModelTransformer(new CallbackTransformer(
function ($array) {
// your implementation here
},
function ($array) {
if (!$array) {
return;
}
return new ArrayCollection(array_map(function ($data) {
return $this->someRepository->findOneBy(['FIELD' => $data]);
}, $array));
}
));
....

How to change order of symfony form elements?

I have simple form MyForm1:
class MyForm1 extends Symfony\Component\Form\AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('field1', TextType::class, [
'label' => 'Field1'
])
$builder->add('save', SubmitType::class, [
'label' => 'Save',
]);
}
}
and form MyForm2 that is inherited from MyForm1
class MyForm2 extends MyForm1
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('field2', TextType::class, [
'label' => 'Field2'
])
}
}
and template
{{ form(form) }}
When I display MyForm2, save button is displayed before field2. How I can change order of elements? I know that I can remove and add this button again. Or call render function for every element in template. But I want just to set render indexes for elements. Suppose that it will take less time. Is it possible?
Thanks
In my opinion the order of your form fields is something related to the view, not the model. So you should display manually your form in your view, fields by fields.
{{ form_start(form) }}
{{ form_row(form.field1)
{{ form_row(form.field2)
{{ form_row(form.save)
{{ form_end(form) }}
class MyForm1 extends Symfony\Component\Form\AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$this->buildFields($builder, $options);
$builder->add('save', SubmitType::class, [
'label' => 'Save',
]);
}
public function buildFields(FormBuilderInterface $builder, array $options)
{
$builder->add('field1', TextType::class, [
'label' => 'Field1'
])
}
}
class MyForm2 extends MyForm1
{
// You can actually completely skip this method
/*public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
}*/
public function buildFields(FormBuilderInterface $builder, array $options)
{
parent::buildFields(FormBuilderInterface $builder, array $options);
$builder->add('field2', TextType::class, [
'label' => 'Field2'
]);
}
}
You can also add buildSubmit in the same fashion if you need.