Here is my case:
I have two fields in a form, the second field values depends on the value selected on the first field.
I'm doing this to modify the second field:
$builder->add('firstField',EntityType::class, array(class => First::class));
$formModifier = function(FormInterface $formBuilder, $data = null){
if($data != null){
$formBuilder->add('secondField', EntityType::class, array(
class => Second::class,
'query_builder' => function (EntityRepository $er) use ($data) {
return $er->createQueryBuilder('s')
->where('s.field = :data')
->setParameter('data', $data)
;
});
} else {
$formBUilder->add('secondField', HiddenType::class, array('required' => false);
}
}
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getData());
}
);
$builder->get('firstField')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier){
$data = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $data);
}
);
Ajax:
var $firstField = $('#firstField');
$(document).on('change', '#firstField', function(){
var $form = $(this).closest('form');
var data = {};
data['id'] = $(this).val();
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
$('#secondField').html(
$(html).find('#secondField').html()
);
}
})
});
The data is sent correctly via ajax, but i never get the correct response because it doesn't ever enter in the POST_SUBMIT event listener as i tried to debug and use some var_dump to check it. Any help would be appreciated. Thanks.
Related
I have a form that contains 3 fields (date, typeEvent, seller) where Seller is a choiceType that depends on date and typeEvent, and to do that i followed the symfony documentation for dynamics forms.
but the exemple in the doc its about a field that depends on only one other field.
what i did so far :
$formModifier = function (FormInterface $form,DateTime $date = null, TypeEvent $type = null) {
if (($date === null) || ($type === null)) {$sellers = [];return;}
$repo = $this->entityManager->getRepository(User::class);
$start = $date->format("Y-m-d H:i:s");
$end = new DateTime($date->format("Y-m-d H:i:s"));
$end = date_add($end,date_interval_create_from_date_string("60 minutes"))->format('Y-m-d H:i:s');
$organisation = $this->security->getUser()->getOrganisation();
$sellers = $repo->findSellers($organisation,$start,$end);
$form->add('seller', EntityType::class, [
'class' => User::class,
'placeholder' => '',
'choices' => $sellers,
'choice_label' => 'pseudo',
'attr' => ['class'=>'seller-select'],
'required'=>false,
'expanded' =>false,
]);
};
$builder->get('start')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$start = $event->getForm()->getData();
$type = $event->getForm()->getParent()->getData()->getTypeEvent();
$formModifier($event->getForm()->getParent(), $start, $type);
}
);
$builder->get('typeEvent')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$type = $event->getForm()->getData();
$start = $event->getForm()->getParent()->getData()->getStart();
$formModifier($event->getForm()->getParent(), $start, $type);
}
);
the problem here is that, for exemple when i try to add a listener to 'start' field inside of it, i don't have access to the other fields, the typeEvent field specifically, i tried $event->getForm()->getParent()->getData()->getTypeEvent() but it returns null, and that's $event->getForm()
dumped.
As you can see the $event->getForm()->getParent()->getData() it's like a new Event() with all attribute on null.
So my question is: There is any way to get the typeEvent there ? or should i proceed differently?
Thank you.
I am not completely sure if this is what you want, but you should take a look at this answer:
https://stackoverflow.com/a/25890150/17089665
$form->all(); <- Will get you all fields
$child->getName(); <- Will get you the name of every child, if you iterate the $form variable.
Based on documentation: http://symfony.com/doc/2.8/form/dynamic_form_modification.html#form-events-submitted-data
I prepared dynamic generated form. And everything works properly but only when I use form for adding new data (/new) when I use the same form for editing existing data - not working
Simple form for "Appointment". It should work like that: User select client and then second "select" is filling proper data - depends on each client from first select. And this works ok but only when I try add new Appointment. When I try edit no.
class AppointmentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('client', EntityType::class, array(
'class' => 'SystemAdminBundle:Client',
'placeholder' => '',
));
$formModifier = function(\Symfony\Component\Form\FormInterface $form, Client $client)
{
$diseases = array();
if($client !== null) {
$diseases = $client->getDiseases();
}
$form->add('disease', EntityType::class, array(
'class' => 'SystemAdminBundle:Disease',
'placeholder' => '',
'choices' => $diseases,
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getClient());
}
);
$builder->get('client')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$client = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $client);
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'System\AdminBundle\Entity\Appointment'
));
}
}
Appointment controller - here is function for add new appointment and edit. For "new" my code works, for "edit" no.
public function newAction(Request $request)
{
$appointment = new Appointment();
$form = $this->createForm(AppointmentType::class, $appointment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $request->request->get('appointment');
if(array_key_exists('name', $data)) {
$em = $this->getDoctrine()->getManager();
$em->persist($appointment);
$em->flush();
return $this->redirectToRoute('appointment_show', array('id' => $appointment->getId()));
}
}
return $this->render('appointment/new.html.twig', array(
'appointment' => $appointment,
'form' => $form->createView(),
));
}
public function editAction(Request $request, Appointment $appointment)
{
$deleteForm = $this->createDeleteForm($appointment);
$appointment = new Appointment();
$editForm = $this->createForm('System\AdminBundle\Form\AppointmentType', $appointment);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$data = $request->request->get('appointment');
if(array_key_exists('name', $data)) {
$em = $this->getDoctrine()->getManager();
$em->persist($appointment);
$em->flush();
return $this->redirectToRoute('appointment_show', array('id' => $appointment->getId()));
}
return $this->redirectToRoute('appointment_edit', array('id' => $appointment->getId()));
}
return $this->render('appointment/edit.html.twig', array(
'appointment' => $appointment,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
View for "new" appointment
{% block content %}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
window.onload = function() {
var $sport = $('#appointment_client');
$sport.change(function() {
var $form = $(this).closest('form');
var data = {};
data[$sport.attr('name')] = $sport.val();
data['appointment[_token]'] = $('#appointment__token').val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(html) {
$('#appointment_disease').replaceWith(
$(html).find('#appointment_disease')
);
}
});
});
};
{% endblock %}
View for "edit" appointment - it's almost the same as for "new" appointment
{% block content %}
{{ form_start(edit_form) }}
{{ form_widget(edit_form) }}
{{ form_end(edit_form) }}
window.onload = function() {
var $sport = $('#appointment_client');
$sport.change(function() {
var $form = $(this).closest('form');
var data = {};
data[$sport.attr('name')] = $sport.val();
data['appointment[_token]'] = $('#appointment__token').val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(html) {
$('#appointment_disease').replaceWith(
$(html).find('#appointment_disease')
);
}
});
});
};
{% endblock %}
You create a new Appointment in your editAction and then persist it. You should take the one that's in your function parameters, handle the request and just flush, since your object is already persisted.
So remove these lines :
$appointment = new Appointment();
// ...
$em->persist($appointment);
I'm starting to investigate Symfony2 framework and it seems that I've messed up some things in my mind.
I am trying to do a relatively simple thing; Dynamically add an extra field to a form depending on the value of a select box. Let's assume for simplicity that I don't have my form attached to any entity, so what I eventually want to get back is an array of values.
So, as a simple example I would have this controller:
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
$form = $this->createFormBuilder(null, array('allow_extra_fields' => true))
->add('test', 'Symfony\Component\Form\Extension\Core\Type\TextType')
->add('select', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array(
'choices' => array(
'Maybe' => null,
'Yes' => true,
'No' => false,
),
'choices_as_values' => true,
))
->add('submit', 'Symfony\Component\Form\Extension\Core\Type\SubmitType')
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
dump($form->getData());
die;
}
return $this->render('default/index.html.twig', array(
'form' => $form->createView()
));
}
/**
* #Route("/test", name="test")
*/
public function AddFieldAction(Request $request)
{
return new JsonResponse($request->get('form'));
}
And the javascript of the view could be:
$('document').ready(function(){
$('#form_select').change(function(){
var form = $(this).closest('form');
var value = $(this).val();
var data = {};
data[$(this).attr('name')] = value;
$.ajax({
url: "{{ url("test") }}",
type: 'POST',
data: data,
dataType: 'json'
}).done(function (response) {
console.log(response);
if (response.select == 1) {
$('#form').append("<label> extra field </label> <input type='text' id='extra_field' name="form[extra_field]"></input>");
}
}).fail(function (jqXHR, textStatus) {
});
})
})
Now this would return the field correctly, as this would invoke the AddFieldAction and it would add the appropriate HTML in the form. When i would try to submit the form, in the $form->isValid part, I could get all the submitted data with the $form->getData() and $form->getExtraData() methods.
Question is, is this the optimal way to do it? Should (and could) I use Form Events instead? I was trying to use form events but it becomes a mess and I get lost easily in the hierarchy of PRE_SUBMIT or PRE_SET_DATA conficting each other.
I am attempting to create a dynamic form, much like Symfony's example of dynamic form modification
I have a parent form Type of "AttributeType" which contains a collection of "AttributeDetailTypes". The child AttributeDetailType() form is where I have my listeners set up:
$attributeDetailFields = $options['attributeDetailFields'];
$builder
->add('attributeDetailField', 'entity', array(
'class' => 'RelayRiseBundle:AttributeDetailField',
'choices' => $attributeDetailFields,
'property' => 'name'
));
My listeners will populate a field 'value' with a field type based on the user's selected 'attributeDetailField':
$formModifier = function (FormInterface $form, AttributeDetailField $attributeDetailField = null) {
print($attributeDetailField->getName());
if($attributeDetailField->getDataType()->getValue() === 'date') {
$form->add('value', 'collot_datetime', array('pickerOptions' =>
array("format" => "mm/dd/yyyy",
"todayBtn" => "linked",
"autoclose" => true,
"minView" => "month",
"todayHighlight" => true),
));
} else {
$form->add('value', 'text');
}
}
So I have PRE_SET_DATA and POST_SUBMIT event listeners.
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
if (null != $event->getData()) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getAttributeDetailField());
}
}
);
$builder->get('attributeDetailField')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
if (null != $event->getData()) {
$attributeDetailField = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $attributeDetailField);
}
}
);
The PRE_FORM_DATA listener works great when the form is populated. The problem is when I do my AJAX callback to the controller which triggers a form->submit. Then the POST_SUBMIT listener AND the PRE_FORM_DATA listener are called, and I get the following error:
"A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead"
How can I submit my form via Ajax, trigger my POST_SUBMIT listener without throwing the PRE_SET_DATA error?
I have three entities: Country, State and City with the following relationships:
When creating a City, I want two selectors, one for the Country and one for the State where the city belongs. These two selectors need to be chained so changing the Country will "filter" the States shown in the other selector.
I found a tutorial showing how to do this using Form Events but their example it's not quite my case. My entity City it's not directly related to the Country entity (they are indirectly related through State) so, when setting the country field in the City form (inside the class CityType), I'm forced to declare that field as 'property_path'=>false as you can see in the code below:
class CityType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('country', 'entity', array(
'class'=>'TestBundle:Country',
'property'=>'name',
'property_path'=>false //Country is not directly related to City
));
$builder->add('name');
$factory = $builder->getFormFactory();
$refreshStates = function ($form, $country) use ($factory)
{
$form->add($factory->createNamed('entity', 'state', null, array(
'class' => 'Test\TestBundle\Entity\State',
'property' => 'name',
'query_builder' => function (EntityRepository $repository) use ($country)
{
$qb = $repository->createQueryBuilder('state')
->innerJoin('state.country', 'country');
if($country instanceof Country) {
$qb->where('state.country = :country')
->setParameter('country', $country);
} elseif(is_numeric($country)) {
$qb->where('country.id = :country')
->setParameter('country', $country);
} else {
$qb->where('country.name = :country')
->setParameter('country', "Venezuela");;
}
return $qb;
}
)));
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates)
{
$form = $event->getForm();
$data = $event->getData();
if($data == null)
return;
if($data instanceof City){
if($data->getId()) { //An existing City
$refreshStates($form, $data->getState()->getCountry());
}else{ //A new City
$refreshStates($form, null);
}
}
});
$builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates)
{
$form = $event->getForm();
$data = $event->getData();
if(array_key_exists('country', $data)) {
$refreshStates($form, $data['country']);
}
});
}
public function getName()
{
return 'city';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Test\TestBundle\Entity\City');
}
}
The problem is that when I try to edit an existing City, the related Country is not selected by default in the form. If I remove the line 'property_path'=>false I get (not surprisingly) the error message:
Neither property "country" nor method "getCountry()" nor method "isCountry()" exists in class "Test\TestBundle\Entity\City"
Any ideas?
OK, I finally figured out how to do it properly:
namespace Test\TestBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Event\DataEvent;
use Test\TestBundle\Entity\Country;
use Test\TestBundle\Entity\State;
use Test\TestBundle\Entity\City;
class CityType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
$factory = $builder->getFormFactory();
$refreshStates = function ($form, $country) use ($factory) {
$form->add($factory->createNamed('entity','state', null, array(
'class' => 'Test\TestBundle\Entity\State',
'property' => 'name',
'empty_value' => '-- Select a state --',
'query_builder' => function (EntityRepository $repository) use ($country) {
$qb = $repository->createQueryBuilder('state')
->innerJoin('state.country', 'country');
if ($country instanceof Country) {
$qb->where('state.country = :country')
->setParameter('country', $country);
} elseif (is_numeric($country)) {
$qb->where('country.id = :country')
->setParameter('country', $country);
} else {
$qb->where('country.name = :country')
->setParameter('country', null);
}
return $qb;
})
));
};
$setCountry = function ($form, $country) use ($factory) {
$form->add($factory->createNamed('entity', 'country', null, array(
'class' => 'TestBundle:Country',
'property' => 'name',
'property_path' => false,
'empty_value' => '-- Select a country --',
'data' => $country,
)));
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates, $setCountry) {
$form = $event->getForm();
$data = $event->getData();
if ($data == null) {
return;
}
if ($data instanceof City) {
$country = ($data->getId()) ? $data->getState()->getCountry() : null ;
$refreshStates($form, $country);
$setCountry($form, $country);
}
});
$builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates) {
$form = $event->getForm();
$data = $event->getData();
if(array_key_exists('country', $data)) {
$refreshStates($form, $data['country']);
}
});
}
public function getName()
{
return 'city';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Test\TestBundle\Entity\City');
}
}
The jQuery AJAX selector
$(document).ready(function () {
$('#city_country').change(function(){
$('#city_state option:gt(0)').remove();
if($(this).val()){
$.ajax({
type: "GET",
data: "country_id=" + $(this).val(),
url: Routing.generate('state_list'),
success: function(data){
$('#city_state').append(data);
}
});
}
});
});
I hope this will be helpful to somebody else facing the same situation.
Since your link to this approach is down i decided to complement your excelent answer so anyone can use it:
In order to execute the following javascript command:
url: Routing.generate('state_list'),
You need to install FOSJsRoutingBundle that can be found in here.
ATENTION: in the read me section of the bundle there are instalation instructions but there is something missing. If you use the deps with this:
[FOSJsRoutingBundle]
git=git://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git
target=/bundles/FOS/JsRoutingBundle
You must run the php bin/vendors update before the next steps.
I'm still trying to find out what route is needed in the routing.yml for this solution to work. As soon as i discover i will edit this answer.
You will need a dedicated FieldType for the chained selectbox. And also an xhr controller that can return child options based on passed parameter. Ofcourse property_path should be set to false.