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.
Related
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.
We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html
I have article entity, and I want to add one article via ajax.
I create a form:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name')
->add('descriptions');
}
This is my controller:
/**
* #Route("/admin/article/add", name="app_admin_add_article")
*/
public function addCustomer(Request $request)
{
$form = $this->createForm(ArticleFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
/** #var Article $article */
$article = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($article);
$em->flush();
$this->addFlash('success', 'Article was successfully added!');
return $this->redirectToRoute('app_admin_articles');
}
return $this->render('admin/articles/create.html.twig', [
'articleForm' => $form->createView(),
]);
}
How can I write the javascript to submit this form via ajax?
Call your ajax on your html using a button or another element
$.ajax({
type: 'POST',
url: "{{ url('app_admin_add_article') }}",
data: {
name: document.getElementById("nameInput").value,
description: document.getElementById("descriptionInput").value
},
success: function(data) {
alert(data);
},
error: function(err) {
alert(err);
}
});
and get the data sent by ajax in your controller
/**
* #Route("/admin/article/add", name="app_admin_add_article")
*/
public function addCustomer(Request $request)
{
// throw exception if not ajax call
if (!$request->isXmlHttpRequest()) {
throw new NotFoundHttpException();
}
$name = $request->request->get('name');
$description = $request->request->get('description');
$article = new Article();
$article->setName($name);
$article->setDescription($description);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($article);
$entityManager->flush();
return new Response('Article created');
}
This is just a basic example without validating the input values and considering your class extends Controller
i guess you ajax request should look somthing like this :
$.ajax({
type: 'POST',
url: $("form").attr("action"),
data: $("form").serialize(),
//or your custom data either as object {foo: "bar", ...} or foo=bar&...
success: function(response) { ... },
});
and you have to fire it with on click function on the submit button after preventing default for the button :
e.preventDefault();
and in this case you have to change your controller logic.
but if i may ask why do you want to submit with ajax ?
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;
I have created a custom list view in sonata admin to display a calendar.
I'm trying to add events to the calendar dynamically, but I'm getting an error with the CSRF token being invalid.
I have the following code:
public function listAction()
{
if (false === $this->admin->isGranted('LIST')) {
throw new AccessDeniedException();
}
$datagrid = $this->admin->getDatagrid();
$formView = $datagrid->getForm()->createView();
// set the theme for the current Admin Form
$this->get('twig')->getExtension('form')->renderer->setTheme($formView, $this->admin->getFilterTheme());
$em = $this->getDoctrine()->getManager();
$events = $em->getRepository('BMCrmBundle:Event')->findAll();
$event = new Event();
$formEvent = $this->createForm(new EventType(), $event );
return $this->render($this->admin->getTemplate('list'), array(
'action' => 'list',
'form' => $formView,
'datagrid' => $datagrid,
'csrf_token' => $this->getCsrfToken('sonata.batch'),
'events' => $events,
'formEvent' => $formEvent->createView()
));
}
view
var url = "{{ path('create_event', { _sonata_admin: 'bm.crm.admin.event'} ) }}";
$.post(url, form.serialize(), function(data) {
alert(data);
});
This always returns that the CSRF token is invalid
Any ideas?
Check if in your view, you have the following line:
{{ form_rest(form) }}
because I believe that you are rendering form fields one by one and not the whole form at once and forgot to render the rest of the form, which contains the CSRF token.