Validating dynamically loaded choices in Symfony 2 - symfony

I have a choice field type named *sub_choice* in my form whose choices will be dynamically loaded through AJAX depending on the selected value of the parent choice field, named *parent_choice*. Loading the choices works perfectly but I'm encountering a problem when validating the value of the sub_choice upon submission. It gives a "This value is not valid" validation error since the submitted value is not in the choices of the sub_choice field when it was built. So is there a way I can properly validate the submitted value of the sub_choice field? Below is the code for building my form. I'm using Symfony 2.1.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parent_choice', 'entity', array(
'label' => 'Parent Choice',
'class' => 'Acme\TestBundle\Entity\ParentChoice'
));
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => array(),
'virtual' => true
));
}

To do the trick you need to overwrite the sub_choice field before submitting the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$parentChoice = $event->getData();
$subChoices = $this->getValidChoicesFor($parentChoice);
$event->getForm()->add('sub_choice', 'choice', [
'label' => 'Sub Choice',
'choices' => $subChoices,
]);
});
}

this accept any value
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if(is_array($data['tags']))$data=array_flip($data['tags']);
else $data = array();
$event->getForm()->add('tags', 'tag', [
'label' => 'Sub Choice',
'choices' => $data,
'mapped'=>false,
'required'=>false,
'multiple'=>true,
]);
});

Adding an alternate approach for future readers since I had to do a lot of investigation to get my form working. Here is the breakdown:
Adding a "New" option to a dropdown via jquery
If "New" is selected display new form field "Custom Option"
Submit Form
Validate data
Save to database
jquery code for twig:
$(function(){
$(document).ready(function() {
$("[name*='[custom_option]']").parent().parent().hide(); // hide on load
$("[name*='[options]']").append('<option value="new">New</option>'); // add "New" option
$("[name*='[options]']").trigger("chosen:updated");
});
$("[name*='[options]']").change(function() {
var companyGroup = $("[name*='[options]']").val();
if (companyGroup == 'new') { // when new option is selected display text box to enter custom option
$("[name*='[custom_option]']").parent().parent().show();
} else {
$("[name*='[custom_option]']").parent().parent().hide();
}
});
});
// Here's my Symfony 2.6 form code:
->add('options', 'entity', [
'class' => 'Acme\TestBundle\Entity\Options',
'property' => 'display',
'empty_value' => 'Select an Option',
'mapped' => true,
'property_path' => 'options.optionGroup',
'required' => true,
])
->add('custom_option', 'text', [
'required' => false,
'mapped' => false,
])
To handle the form data we need to use the PRE_SUBMIT form event.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (isset($data['options']) && $data['options'] === 'new') {
$customOption = $data['custom_option'];
// todo: handle this better on your own
if (empty($customOption)) {
$form->addError(new FormError('Please provide a custom option'));
return;
}
// Check for a duplicate option
$matches = $this->doctrine->getRepository('Acme\TestBundle\Entity\Options')->matchByName([$customOption]);
if (count($matches) > 0) {
$form->addError(new FormError('Duplicate option found'));
return;
}
// More validation can be added here
// Creates new option in DB
$newOption = $this->optionsService->createOption($customOption); // return object after persist and flush in service
$data['options'] = $newOption->getOptionId();
$event->setData($data);
}
});
Let me know if ya'll have any questions or concerns. I know this might not be the best solution but it works. Thanks!

you cannot not build the sub_choice validation because during you config its validator you don't know which values are valid (values depend on value of parent_choice).
What you can do is to resolve parent_choice into entity before you make new YourFormType() in your controller.
Then you can get all the possible values for sub_choice and provide them over the form constructor - new YourFormType($subChoice).
In YourFormType you have to add __construct method like this one:
/**
* #var array
*/
protected $subChoice = array();
public function __construct(array $subChoice)
{
$this->subChoice = $subChoice;
}
and use provided values in form add:
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => $this->subChoice,
'virtual' => true
));

Suppose for sub choices you have id's right ?
Create and empty array with a certain number of values and give it as a choice
$indexedArray = [];
for ($i=0; $i<999; $i++){
$indexedArray[$i]= '';
}
then 'choices' => $indexedArray, :)

Related

Symfony Form - Add required fields to form depending on other field value

I want to add some required fields to my form when an other field has some value. I've tried to do it with PRE_SET_DATA event but I cannot get data in my event.
My example here is to add partner name field when a user is married.
My UserType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('familyStatus', ChoiceType::class, [
'label' => 'Statut de famille',
'label_attr' => [
'class' => 'fg-label'
],
'attr' => [
'class' => 'sc-gqjmRU fQXahQ'
],
'required' => true,
'choices' => [
'Married' => 'M',
'Single' => 'S'
]
])
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$user = $event->getData();
$status = $user->getFamilyStatus(); // Give me NULL
//$status = $form->get('familyStatus')->getData() Give me NULL too
/*
if ($user && $status === 'M') {
$form->add('partnerName', TextType::class, [
'required' => true,
'mapped' => false
]);
)
*/
})
;
}
What's wrong ? How can I add dynamically new fields depending on other field ?
I also tried with POST_SET_DATA but it's not working.
You need the second example from this part of the docs link.
Basically you set the event listener to the entire form. You should add another listener to the field itself with POST_SUBMIT event.

Symfony getData event subscriber is null

I know this question has been asked already a couple of times, but there hasn't been an answer that actually helped me solving my problem.
I've got three EventSubscribers for three Dropdowns who are dependent on each other.
So in my FormType I say:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// solution showmethecode
$pathToAgencies = 'agencies';
//
$builder
->addEventSubscriber(new AddChannel1Subscriber($pathToAgencies))
->addEventSubscriber(new AddChannel3Subscriber($pathToAgencies))
->addEventSubscriber(new AddAgencySubscriber($pathToAgencies));
}
and one of my EventSubscribers looks like that:
...
...
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addChannel1Form($form, $channel1s = null) {
$formOptions = array(
'class' => 'AppBundle:Channel1',
'property' => 'name',
'label' => 'label.channel1s',
'empty_value' => 'label.select_channel1s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'multiple' => true,
'required' => false,
'attr' => array(
'class' => 'channel1s'
),
);
if ($channel1s){
$formOptions['data'] = $channel1s;
}
$form->add('channel1s', 'entity', $formOptions);
}
public function preSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$accessor = PropertyAccess::createPropertyAccessor();
$agency = $accessor->getValue($data, $this->pathToAgency);
$channel1s = ($agency) ? $agency->getChannel3s()->getChannel1s() : null;
$this->addChannel1Form($form, $channel1s);
}
public function preSubmit(FormEvent $event) {
$form = $event->getForm();
$this->addChannel1Form($form);
}
...
Now I'm getting the error "Attempted to call an undefined method named "getChannel3s" of class "Doctrine\Common\Collections\ArrayCollection"." and (I think) this is because my $data in my preSetData is NULL but I don't know why it's null. Am I looking at the wrong spot or where is my mistake here?
preSetData is executed before the original data (which shall be modified if given) is bound to the form ( which is then stored in $options['data']).
The "data" in preSetData is the one you provide to createForm($type, $data = null, array $options = array()).
So before this is set -> the form obviously doesn't have any data and the event-data isn't set either. That's why $data is null inside your listener's onPreSetData method.
You're using the wrong event. Use preSubmit and build your logic around the data submitted by the user ($event->getData()). This will solve your issue.
Quick overview:
onPreSubmit:
$form->get('someButton')->isClicked() returns false
$event->getForm()->getData() returns $options['data'] if any or $options['empty_data']
$event->getData returns the submitted data (array)
you can use setData()
you can add/remove fields
onSubmit:
You can't use setData() here as data was already bound to the form
$form->isSubmitted() still returns false
$form->get('someButton')->isClicked() returns true
You can still add/remove fields
onPostSubmit:
$form->isSubmitted() returns true
"You cannot remove children from a submitted form"
"You cannot add children to a submitted form"
$form->get('someButton')->isClicked() returns true
In the preSetData declaration you get the bad class. Try this :
public function preSetData(GenericEvent $event)
Add the next use :
use Symfony\Component\EventDispatcher\GenericEvent;

Dynamic Generation for Submitted Forms with Form events

I've a little problem with FormEvents, I want do 3 fields populated dynamically.
I explain, I've 3 fields: Project > Box > Cell, the user choose a Project, the Box list is updated, he choose a Box, the cell list is updated.
To do it, I use FormEvent like the documentation say (http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data)
But I've a problem, for just one field dynamically updated, it's work, but no for 2 fields... Actually a user can choose a project, and when he does it, the box field is updated. But, when he choose a box, the cell field wasn't updated...
But, I've find something, who permit it to work, just change something in a ->add() and inversed to ->add(). But I don't want it.
My code is:
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
And when I change it to:
builder
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
// 'choices' => [],
))
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
It's work... But I want an empty list for box at the start, and I want project before box...
A little precision, this form is embded in an other form as a CollectionType.
All the code of this Type:
<?php
namespace AppBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TubeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
// MODIFIER
$boxModifier = function (FormInterface $form, $project) {
$boxes = (null === $project) ? [] : $project->getBoxes();
$form->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => $boxes,
));
};
$cellModifier = function(FormInterface $form, $box) {
$cells = (null === $box) ? [] : $box->getEmptyCells();
$form->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
'choices' => $cells,
));
};
// FORM EVENT LISTENER
$builder->get('project')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($boxModifier) {
$project = $event->getForm()->getData();
$boxModifier($event->getForm()->getParent(), $project);
}
);
$builder->get('box')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($cellModifier) {
$box = $event->getForm()->getData();
$cellModifier($event->getForm()->getParent(), $box);
}
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tube'
));
}
}
Thanks a lot to your help :)
You should use $builder->addEventListener. For multiple fields all you need to do is to have dynamic fields inside FormEvents::PRE_SET_DATA event handler. Also, use parent field data, as explained in the doc to fetch child field choices.
I have used this approach, for generating Country, State and City Entities in hierarchical fields. Let me know if it helps or you need more information.
EDIT : For bigger logic, you can use eventSubscriber which will keep your code clean and you also get to re-use dynamic part of the form for somewhere else.
For multiple dependent hierarchical fields, just add them through conditions in the eventSubscriber class.
Update with code snippet :
Here is a walk through on code snippet that worked for me in Symfony 2.7
Note : I don't replace the dynamic html field as described in the document, instead via jQuery I simply collect child options as per selected parent option and fill in those. When submitted, The form recognises correct child options as per eventSubscriber context. So here is how you might do it :
In your parent Form type (where you have all 3 fields) call a eventSubscriber instead of defining those 3 fields :
$builder->add(); // all other fields..
$builder->addEventSubscriber(new DynamicFieldsSubscriber());
Create an eventSubscriber as defined in the doc, here the file name is DynamicFieldsSubscriber
<?php
namespace YourBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormInterface;
class DynamicFieldsSubscriber implements EventSubscriberInterface
{
/**
* Define the events we need to subscribe
* #return type
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData', // check preSetData method below
FormEvents::PRE_SUBMIT => 'preSubmitData', // check preSubmitData method below
);
}
/**
* Handling form fields before form renders.
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$location = $event->getData();
// Location is the main entity which is obviously form's (data_class)
$form = $event->getForm();
$country = "";
$state = "";
$district = "";
if ($location) {
// collect preliminary values for 3 fields.
$country = $location->getCountry();
$state = $location->getState();
$district = $location->getDistrict();
}
// Add country field as its static.
$form->add('country', 'entity', array(
'class' => 'YourBundle:Country',
'label' => 'Select Country',
'empty_value' => ' -- Select Country -- ',
'required' => true,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.status = ?1')
->setParameter(1, 1);
}
));
// Now add all child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Handling Form fields before form submits.
* #param FormEvent $event
*/
public function preSubmitData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// Here $data will be in array format.
// Add property field if parent entity data is available.
$country = isset($data['country']) ? $data['country'] : $data['country'];
$state = isset($data['state']) ? $data['state'] : $data['state'];
$district = isset($data['district']) ? $data['district'] : $data['district'];
// Call methods to add child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Method to Add State Field. (first dynamic field.)
* #param FormInterface $form
* #param type $country
*/
private function addStateField(FormInterface $form, $country = null)
{
$countryCode = (is_object($country)) ? $country->getCountryCode() : $country;
// $countryCode is dynamic here, collected from the event based data flow.
$form->add('state', 'entity', array(
'class' => 'YourBundle:State',
'label' => 'Select State',
'empty_value' => ' -- Select State -- ',
'required' => true,
'attr' => array('class' => 'state'),
'query_builder' => function (EntityRepository $er) use($countryCode) {
return $er->createQueryBuilder('u')
->where('u.countryCode = :countrycode')
->setParameter('countrycode', $countryCode);
}
));
}
/**
* Method to Add District Field, (second dynamic field)
* #param FormInterface $form
* #param type $state
*/
private function addDistrictField(FormInterface $form, $state = null)
{
$stateCode = (is_object($state)) ? $state->getStatecode() : $state;
// $stateCode is dynamic in here collected from event based data flow.
$form->add('district', 'entity', array(
'class' => 'YourBundle:District',
'label' => 'Select District',
'empty_value' => ' -- Select District -- ',
'required' => true,
'attr' => array('class' => 'district'),
'query_builder' => function (EntityRepository $er) use($stateCode) {
return $er->createQueryBuilder('s')
->where('s.stateCode = :statecode')
->setParameter('statecode', $stateCode);
}
));
}
}
After this, you need to write jQuery events which should update child options on change of parent option explicitly, You shouldn't face any error on submission of the form.
Note : The code above is extracted and changed for publishing here. Take care of namespace and variables where ever required.

How to set select option disabled in formtype disabled dynamically?

I have a form type in symfony2.5 that has choice type which has many options set dynamically as
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('service_id', 'choice', array(
'label' => 'Service',
'choices' => $this->getChoice()
));
.......
.......
}
protected function getChoice()
{
$choices = array();
$options = array(
'1' => 'test1',
.............
);
foreach($options as $key => $option)
{
if($key%2 == 0) {
....
} else {
////disabled choice
}
}
}
How can i set this choices set disabled??
This can be done using form event listeners and finishView method. Similar question has been answered before here - How to disable specific item in form choice type?
Note that PRE_BIND event is deprecated. Use PRE_SUBMIT instead.
Quote:
The events PRE_SUBMIT, SUBMIT and POST_SUBMIT were introduced in
Symfony 2.3. Before, they were named PRE_BIND, BIND and POST_BIND.
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html

Disabling some checkboxes of choice widget in buildForm()

I have a form widget of type "choice" which is being displayed as a list of many checkboxes. Everything works great. So to stress it out: there is ONE widget, with MANY checkboxes (and NOT several checkbox widgets).
Now, I want to disable some of those checkboxes. The data for that is avaliable in the $options-Array.
Here is the buildForm()-function of my FooType.php
...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foo', 'choice', array('choices' => $options['choiceArray']['id'],
'multiple' => true,
'expanded' => true,
'disabled' => $options['choiceArray']['disabled'] // does not work (needs a boolean)
'data' => $options['choiceArray']['checked'], // works
'attr' => array('class' => 'checkbox')))
;
}
...
My Twig-template looks like this:
{% for foo in fooForm %}
<dd>{{ form_widget(foo) }}</dd>
{% endfor %}
I can only disable ALL the checkboxes (by setting 'disabled' => true in buildForm). And passing an array there does not work (as commented in the snippet).
How can I disable some selected checkboxed (stored in $options['choiceArray']['disabled']) of my choice widget?
I have solved the problem using JQuery.
in my FooType.php I stringify the Array of fields that should be disabled.
I pass that string in the buildForm()-Function via a hidden field to the template
there I use JQuery to split the string again into IDs and process disable the checkboxes and grey out the label
Here is the PHP-Code (FooType.php):
...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$disabledCount = sizeof($options['choiceArray']['disabled']);
$disabledString = '';
for ($i = 0; $i < $disabledCount; $i++)
{
$disabledString .= $options['choiceArray']['disabled'][$i];
if ($i < $disabledCount-1)
{
$disabledString .= '|';
}
}
$builder
->add('foo', 'choice', array('choices' => $options['choiceArray']['id'],
'multiple' => true,
'expanded' => true,
'data' => $options['choiceArray']['checked'],
'attr' => array('class' => 'checkbox')))
->add('foo_disabled', 'hidden', array('data' => $disabledString))
;
}
...
Here is the JavaScript part (Twig-template):
function disableModule()
{
var disabledString = $('#foo_disabled').val();
var disabledArray = disabledString.split('|');
$.each( disabledArray, function( disKey, disVal )
{
// deactivate checkboxes
$('input[id="'+idCurrent+'"]').attr("disabled", true);
// grey out label for checkboxes
$('label[for="'+idCurrent+'"]').attr("style", "color: gray;");
});
}
In my Entity/Foo.php I had to add the property "foo_disabled" of type string with setter and getter methods.
This page is first on Goole search results for 'twig checkbox checked and disabled'
To set some checkbox inputs checked or/and disabled in twig, one could use choice_attr
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, ['label'=>'Login: '])
->add('roles', ChoiceType::class, [
'label'=>'Role: ',
'multiple'=>true,
'expanded'=>true,
'choices'=>['User'=>'ROLE_USER','Admin'=>'ROLE_ADMIN'],
'choice_attr'=> [
'User' => ['disabled'=>'disabled', 'checked'=>'checked'],
]
])
->add('password', PasswordType::class, ['label'=>'Password: '])
->add('register', SubmitType::class, ['label' => 'Register'])
;
}
In this example I set checked and disabled checkbox for ROLE_USER as this is default role.

Resources