I have a form that contains a collection of forms (a Vote with many VoteChoice). The VoteChoiceType is as follows
class VoteChoiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('answer', null, array('disabled' => true))
->add('priority', null);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'PollBundle\Entity\VoteChoice',
));
}
}
Now in my Controller I create and populate many VoteChoices, setting the answer according to the available choices for the current poll (derived from the URL)
$vote = new Vote();
$vote->setPoll($poll);
foreach ($vote->getPoll()->getPollOptions() as $op) {
$vc = New VoteChoice();
$vote->addVoteChoice($vc->setAnswer($op));
}
So when the Form loads, I want all the options to display only - not to be an actual choice, and then the user can set the priority they want. However, the answer is of every single answer I have in my poll_options table (each Poll has many PollOption, similar to how each Vote has many VoteChoice)
Current twig template
<ul class="voteChoices" data-prototype="{{ form_widget(form.voteChoices.vars.prototype)|e('html_attr') }}">
{% for voteChoice in form.voteChoices %}
<li>{{ form_row(voteChoice.answer) }} {{ form_row(voteChoice.priority) }}</li>
{% endfor %}
</ul>
</div>
<p><button type="submit" class="btn btn-success">Go!</button></p>
{{ form_end(form) }}
I want the voteChoice.answer as a plain text (so it's not part of a dropdown - I know I can disable it in the FormBuilder, but I don't want it to appear as part of a drop-down menu, I just want it as plain text)
If I use voteChoice.answer I get the following symfony error
An exception has been thrown during the rendering of a template ("Catchable Fatal Error: Object of class Symfony\Component\Form\FormView could not be converted to string") in poll\vote.html.twig at line 9.
I have a __toString function in my VoteChoice class.
I want the voteChoice.answer as a plain text (so it's not part of a dropdown - I know I can disable it in the FormBuilder, but I don't want it to appear as part of a drop-down menu, I just want it as plain text)
You can access the current data of your form via form.vars.value (Reference):
{{ voteChoice.vars.value.answer }}
This means that voteChoice.vars.value is an instance of PollBundle\Entity\VoteChoice so you can remove the answer field from your form safely if this is not required by edit.
Related
I have two issues when sending a form with the GET method with Symfony 4. This form contains filters and submitting this form updates the list of displayed items according to the selected filters.
The form is built like this:
class MyForm extends AbstractType {
...
public function buildForm(...) {
$builder
->setMethod("GET")
->add(
"first_filter",
ChoiceType::class,
...
)
->add(
"second_filter",
EntityType::class,
...
)
->add(
"button_apply",
SubmitType::class
);
First problem, after sending the form, the URL looks like this:
/action?my_form[first_filter]=...&my_form[second_filter]=...
Is it normal that the form name is included before every field name, and why the URL could not simply be:
/action?first_filter=...&second_filter=...
The second problem is that the submit button is part of the params visible into the URL:
/action?my_form[button_apply]=&...
As far as I know, the submit button itself should not be a parameter ?
Thanks in advance
it's the normal behaviour, and it can be circumvented, but it requires to call createNamed instead of create on a form factory (see ControllerTrait::createForm) ... so Symfony\Component\Form\FormFactory::createNamed())
// at the top: use Symfony\Component\Form\FormFactory
public function yourControllerAction(FormFactory $formFactory, Request $request) {
$form = $formFactory->createNamed(
'', // form name, otherwise form class name camel_cased
YourFormType::class, // your form type
[], // initial data or object or whatever!
[] // form options
);
// rest of the script is identical, it's still a normal form
}
or you don't inject it and do what the trait does:
$form = $this->container->get('form.factory')->createNamed(
// parameters same as before
);
for the button to disappear from the GET/POST, I would advise you to remove the button from your form and instead add it to your template (also increases reusability).
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit">{{ 'your label'|trans }}</button>
{{ form_end(form) }}
which makes the button submit the form, but since it has no name and value it won't add to the data (in contrast to the SubmitType button form type).
I am a beginner in Symfony, so this question might be simple for those who are more experienced in this framework.
I am building forms and I have several possible types of items in forms. At this moment these are:
text
html
image
However, in the future there will be many more items. Previously, the project generated all items into their place (text, html and image) and hidden those which are not needed for the specific form item (maximum one is needed). However, I intend to avoid adding items I do not need. Since I do not know at the point buildForm of an arbitrary item is running whether it is text, html or image, so at this point all of them are added (I know this is counter-intuitive, but this is a code I try to refactor):
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('layoutTypeInput', TextType::class);
$builder->add('blockTypeOutput', EntityType::class, array(
'class' => 'MyPageBundle:BlockTypeOutput',
'choice_label' => 'titleHu',
'required' => false,
'placeholder' => 'Válassz blokk kimenetet!',
'empty_data' => null,
));
$builder->add('text', TextType::class, $this->getBlockTypeOptions('text'));
$builder->add('html', TextareaType::class, $this->getBlockTypeOptions('html'));
$builder->add('image', ImageSelectType::class, $this->getBlockTypeOptions('image'));
$builder->addEventListener(FormEvents::POST_SET_DATA, array($this, 'onPostSetData'));
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
Now, I have a function which removes the unnecessary elements of a form:
private function removeUnnecessaryItems(\Symfony\Component\Form\FormInterface $form, $key)
{
$keys = ['text', 'html', 'image'];
foreach ($keys as $k) {
if ($key !== $k) $form->remove($k);
}
}
And inside onPostSetData I call it like this:
$this->removeUnnecessaryItems($form, $inputObject->getLayoutTypeIdText());
and finally, in the twig I determine what should be generated into the form:
{% for ioLayoutBlock in form.ioLayoutBlocks %}
<div class="row">
<div class="col-xs-12 col-md-3">
{{ form_errors(ioLayoutBlock.layoutTypeInput) }}
{{ioLayoutBlock.layoutTypeInput.vars.label}}
</div>
{{ form_widget(ioLayoutBlock.layoutTypeInput, {'attr' : {'class':'hidden'}}) }}
<div class="col-xs-12 col-sm-6 col-md-5">
{{ form_errors(ioLayoutBlock.blockTypeOutput) }}
{{ form_widget(ioLayoutBlock.blockTypeOutput, {'attr' : {'class':'blockTypeOutput'}}) }}
</div>
<div class="col-xs-12 col-sm-6 col-md-4">
{% if ioLayoutBlock.text is defined %}
{{ form_errors(ioLayoutBlock.text) }}
{{ form_widget(ioLayoutBlock.text, {'attr':{'class':'hidden uniqueInput ioLayoutBlock_text' }}) }}
{% elseif ioLayoutBlock.html is defined %}
{{ form_errors(ioLayoutBlock.html) }}
{% if layout.layoutType.name == 'userHTML' %}
<div class="input-group ioLayoutBlock_html hidden">
<a class="input-group-addon myAdminForm" target="_blank" data-my-href="page/{{ page.id }}/wysiwyg/{{ layout.id }}"><span class="glyphicon glyphicon-pencil"></span></a>
{{ form_widget(ioLayoutBlock.html, {'attr':{'class':'uniqueInput wysiwyg' }}) }}
</div>
{% else %}
{{ form_widget(ioLayoutBlock.html, {'attr':{'class':'hidden uniqueInput wysiwyg ioLayoutBlock_html' }}) }}
{% endif %}
{% elseif ioLayoutBlock.image is defined %}
{{ form_errors(ioLayoutBlock.image) }}
{{ form_widget(ioLayoutBlock.image, {'attr':{'class':'hidden uniqueInput ioLayoutBlock_image' }}) }}
{% endif %}
</div>
</div>
{% endfor %}
and if I load the page, everything is shown correctly, but unfortunately, when I try to submit the form, it gives the error of
This form should not contain extra fields.
as many times as many form items I have. If I comment out the call on removeUnnecessaryItems inside onPostSetData and subsequently remove the conditionals from the twig, like:
{% if ioLayoutBlock.text is defined %}
then everything works, but that's how it worked before the refactor. Ideally I would like to avoid adding so many unnecessary things at buildForm, but I do not know how can I load up any meaningful data there to determine the type of the item. Alternatively I would like to ensure that the items are successfully removed at the events where I do know their type, without the form errors on submit I described above. So, my question is: How can I avoid generating all kinds of unnecessary stuff into my form without being blocked by submit errors?
My approach would be to give a parameter to your form
$form = $this->createForm(DynamicType::class, $user, [
'layout_type_id' => $layoutTypeIdText,
]),
Then adds the fields depending of the param
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->layout_type = $options['LayoutTypeId'];
// [...]
if ($this->layout_type !== 'text' )
$builder->add('text', TextType::class, $this->getBlockTypeOptions('text'));
// [...]
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// [...]
'layout_type_id' => null,
]);
}
With this approch, the good part is you don't to duplicate your logic in twig, the form get the fields it needs, so you can just render the form using
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
For your specific case you need to avoid adding the items at buildForm, to handle the default empty values at onPreSubmit (as the transformers will not be called if the items are not added at buildForm) and to add the effective items at onPreSubmit.
You were correct using form listeners, they are the way to go for your use case.
I see 2 sub scenarii:
the form data is coming from the Model, you fill it inside the controller
a new item is added to the form, client-side (via the prototype).
I'm getting from your example code that users can't add new items dynamically. If you want to do so, the code is just slightly different.
Remains the first scenario. The trick is not to remove, but to add forms inside the PRE_SET_DATA listener:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('layoutTypeInput', TextType::class);
$builder->add('blockTypeOutput', EntityType::class, array(
'class' => 'MyPageBundle:BlockTypeOutput',
'choice_label' => 'titleHu',
'required' => false,
'placeholder' => 'Válassz blokk kimenetet!',
'empty_data' => null,
));
$builder->addEventListener(FormEvents:: PRE_SET_DATA, array($this, 'onPreSetData'));
// ...
}
public function onPreSetData(FormEvent $event)
{
$data = $event->getData(); // This contains model data (ie., from controller)
$form = $event->getForm();
$type = 'image'; // Read type from your model
$formType = $this->getFormTypeForType($type);
$builder->add($type, formType, $this->getBlockTypeOptions($type));
}
private function getFormTypeForType($type)
{
switch ($type) {
case 'image':
return ImageSelectType::class;
// ...
default:
// Up to you, you can decide on setting a default type or enforcing that the type is correct
throw new \RuntimeException('Unsupported type');
}
}
With that code, you can keep the same Twig.
I'm not sure on what you are trying to do with layoutTypeInput and blockTypeOutput. Maybe we are answering just partially here, don't hesitate on posting the full use case.
I tried to follow this answer to handle nested collections in forms.
I have an Application's Form with a collection of LienAppliServ's Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('servLiens', 'collection', array(
'label' => ' ',
'type' => new LienAppliServType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' =>false,
'prototype' => true,
))
//...
In my LienAppliServ's Form, I have another collection of PortLienAppliServ's Form :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('ports', 'collection', array(
'type' => new PortLienAppliServType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' =>false
))
//...
And the form of PortLienAppliServ is :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('numPort')
->add('type')
;
}
Now, I'd like to handle add/delete for each collection...
As I said, I tried to follow this answer
In order to do that, I tried :
{% block body -%}
{{ form_start(form) }}
<ul id="col-servliens" data-prototype="{{ form_widget(form.servLiens.vars.prototype)|e }}">
{# iterate over each existing tag and render its only field: name #}
{% for servLiens in form.servLiens %}
<li>{{ form_row(servLiens) }} </li>
<ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
{%for ports in servLiens.ports %}
<li>{{ form_row(ports) }}</li>
{% endfor %}
{% endfor %}
</ul>
{{ form_end(form) }}
{% endblock %}
{% block app_js %}
//Same as the other question
<script>
function FormCollection(div_id)
{
// keep reference to self in all child functions
var self=this;
self.construct = function () {
// set some shortcuts
self.div = $('#'+div_id);
self.div.data('index', self.div.find(':input').length);
// add delete link to existing children
self.div.children().each(function() {
self.addDeleteLink($(this));
});
// add click event to the Add new button
self.div.next().on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
self.addNew();
});
};
/**
* onClick event handler -- adds a new input
*/
self.addNew = function () {
// Get the data-prototype explained earlier
var prototype = self.div.data('prototype');
// get the new index
var index = self.div.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
self.div.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
self.div.append($(newForm));
// add a delete link to the new form
self.addDeleteLink( $(self.div.children(':last-child')[0]) );
// not a very nice intergration.. but when creating stuff that has help icons,
// the popovers will not automatically be instantiated
//initHelpPopovers();
return $(newForm);
};
/**
* add Delete icon after input
* #param Element row
*/
self.addDeleteLink = function (row) {
var $removeFormA = $('<i class="entypo-trash"></i>');
$(row).find('select').after($removeFormA);
row.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// remove the li for the tag form
row.remove();
});
};
self.construct();
}
</script>
<script>
$(document).ready(function() {
new FormCollection('col-servliens');
new FormCollection('col-ports');
});
</script>
And I get
Variable "ports" does not exist.
I really need some help.. Collections are actually my nightmare...
Thanks !
Let me start with apologies, because my answer in the post you mention was clearly wrong :) At least the example code was incorrect. I wonder if the original poster ever found the right solution.
The error is easy enough to spot. In the Twig you use the variable ports, but how should Twig know where it comes from?
{% for servLiens in form.servLiens %}
<li>{{ form_row(servLiens) }} </li>
<ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
{%for ports in servLiens.ports %}
<li>{{ form_row(ports) }}</li>
{% endfor %}
{% endfor %}
Using {{ form_row(servLiens) }} will actually already create the collection classes, so the whole <ul> inside it is not needed. In fact Symfony is smart enough that you don't even need the first <ul>. It will find any child forms (ex. collections) and create all the HTML needed.
So the only thing you need to have in your code is:
{{ form_row(form.servLiens) }}
If you would look at the HTML generated from that, you will see the data-prototype and the children. It will automatically have an id attribute that you can use.
Problem is the javascript part. You are using a collection within a collection. The javascript FormCollection class (which I wrote) cannot handle this as simply as you've tried it. It will handle the top collection fine, but it will not automatically instantiate collections on load/create/delete. Remember that each servLiens collection has a collection of ports. Therefore already at loading you can't simply load the col-ports as you do now, but would need to load each port collection. Then you'd need to instantiate a new FormCollection on the port collection every time you add a servLiens row.
I don't have time to create your code for you, but I hope my explanation helps you to find a solution.
Looks like you use symfony 2.6 or older.
I would start by saying you should'nt do "'type' => new PortLienAppliServType()" but pass the form type name. Symfony will instantiate it and it could be the problem.
Everything is described in extend here: https://symfony.com/doc/2.6/reference/forms/types/collection.html#basic-usage
Honestly, Collections are not that difficult you just need to understand how it works then Symfony will manage everything for you.
I am trying to develop a form that lets students select courses from several groups. It is defined as follows:
Courses have a name/description/hours.
Courses belong to groups, which have a name/description/limits on the hours (e.g. you must choose 5 credit hours from this group, or you can only take up to 10 hours from that group)
Course Groups are organized into a Program Offering, which could have other fields
So I want the form to allow students to select courses, but the courses are organized in those groups with instructions about how many to pick. The credit hours part will be validated with custom Validation rules.
Here's an idea of the code I'm thinking of (omitting a lot of the namespaces/Doctrine mapping/etc.).
The entities are sort of like this:
class Offering
{
// has multiple CourseGroups
private $groups;
}
class CourseGroup
{
// which Offering it belongs to
private $offering;
private $name;
private $maxHours;
}
class Course
{
// which CourseGroup it belongs to
private $group;
private $name;
private $hours;
}
// User submits an application with chosen courses from multiple groups
class Application
{
// ...
private $courses;
}
// Joins the Applications to Courses (N to N)
class ApplicationCourse
{
// which Application
private $application;
// which Course
private $course;
}
But I'm trying to figure out how to put this in a form. I don't mind just binding the form to an array and sorting it out later to put it in an Application.
class ApplicationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...other fields
;
// Add the groups of courses to choose
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use ($offering) {
$form = $event->getForm();
// Here I would like to add 1 EntityType per Offering
// The EntityType allows the user to select multiple Courses within that group
$i = 0;
foreach ($offering->groups as $group) {
// Maybe add the $group as a static block in the twig template here
// Ideally the form should show the group name/description
// I'll probably borrow from this class https://github.com/genemu/GenemuFormBundle/blob/master/Form/Core/Type/PlainType.php
// This adds a group of checkboxes to select a course from this group
$form->add('course_'.$i, 'entity', array(
'class' => 'Acme\DemoBundle\Entity\Course',
'property' => 'name',
'multiple' => true,
'expanded' => true,
'query_builder' => function(EntityRepository $er) use ($group) {
// imagine this ia a query that selects all courses in the $group
return $er->createGroupCoursesQueryBuilder($group);
},
);
$i++;
}
}
);
}
public function getName()
{
return 'task';
}
}
In the end, I want a form that looks like this:
Science: select at least 4 credit hours
[x] SCI100 (2 hours)
[ ] SCI101 (2 hours)
[ ] SCI102 (2 hours)
Math: select at least 4 credit hours
[ ] MTH100 (4 hours)
[x] MTH101 (4 hours)
{GROUP NAME}: {GROUP DESCRIPTION}
[{selected?}] {COURSE NAME} {COURSE HOURS}
Etc.
Will this method work? Is there a better way so that I don't have to bind that form to an array and then re-assemble it after validation?
The solution was actually simple after seeing this answer Symfony 2 Forms entity Field Type grouping
I just customized the rendering of the form. The best part is, I get all the helpful parts of Symfony forms (the data binding), but I can completely customize the rendering.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('courses', 'entity', array(
'class' => 'MyMainBundle:Course',
'property' => 'name',
'multiple' => true,
'expanded' => true
))
->add('save', 'submit')
;
}
There's a cool trick in the twig template to just rendering it myself. I did have to pass in the groups of courses.
The trick is to use {% do form.courses.setRendered %}
{{ form_start(form) }}
{{ form_errors(form) }}
{% for group in academics.courseGroups %}
<section>
<h2>
{{ group.name }}
</h2>
<p>{{ group.description }}</p>
{% for course in group.courses %}
<div class="form-group">
<div class="course">
<div class="checkbox">
<label for="my_mainbundle_application_course_courses_{{ course.id }}">
<input type="checkbox"
id="my_mainbundle_application_course_courses_{{ course.id }}"
name="my_mainbundle_application_course[courses][]"
value="{{ course.id }}"
{% if course.id in form.courses.vars.value and form.courses.vars.value[course.id] %}checked="checked"{% endif %}>
<span class="name">{{ course.name }}</span>
<span class="subject">({{ course.subject }})</span>
-
<span class="credits">{{ course.credits }} hours</span>
</label>
</div>
</div>
</div>
{% endfor %}
</section>
{% endfor %}
{% do form.courses.setRendered %}
{{ form_row(form.save) }}
{{ form_end(form) }}
I have a controller where I am creating a form witg two dropdown list inside.
When I am rendering my view, I would like to have the same form elements on the top and the bottom of the page. The problem is that the form elemetns (dropdownlists) are displayed only on the top of the page, even if I am asking twig to put them also on the bottom.
Here is what I would like to have:
The 1 and 2 are the dropdownlists. And I would like to duplicate this on the top and on the bottom of the page.
Any Idea on how can this be done?
The top content and the bottom content, where the two dropdownlists are inside are in a single sseparate twig file (searchPanel.html.twig) and this file is included in the page
{% include "MyBundle:Search:searchPanel.html.twig" %}
Here is the searchPanel.html.twig
<div class="searchPanel">
<form action="{{ path }}" method="POST" {{ form_enctype(form) }}>
Papers per page
{{ form_widget(form.papers_per_page, { 'class': 'ppp'}) }}
/ Sort by
{{ form_widget(form.sort_by, { 'class': 'sort'}) }}
{{ form_rest(form) }}
/ Papers ({{ papers_number }} results)
<input type="submit" class="updateSearchResults" value="Update"></input>
</form>
A problem in your approach is that Symfony's Form-component will render the form elements with id's which would be duplicated if you rendered the same form twice on your page. You might also run in trouble with the csrf_token. The gist being that forms are not intended to be duplicated.
Here is what I would do. Create a twig-template containing your paginator form without using Symfony\Form, i.e. create all form elements statically and pass it the paginator-object (or array) to get the data instead of using form_widget(). Something like this:
<form action="{{ path(app.request.attributes.get('_route') }}" method="POST">
<select name="paginator[per_page]">
{% for per_page in paginator.papers_per_page %}
<option value=""{{ per_page }}">{{ per_page }}</option>
{% endfor %}
</select>
</form>
The form action will automatically submit the data to your current route, so you can embed it in different actions and it will submit the data to the same action. On POST you can just create a paginator-object with the post-data and then add it as the form's data. After that you just use isValid() as usual.
In your controller you can get the data like this:
use Symfony\Component\HttpFoundation\Request;
// ...
public function PaperController()
{
public function listAction(Request $request)
{
if ($request->getMethod() == 'POST') {
$data = $request->request->get('paginator');
$paginator = new Paginator($data);
$form = new PaginatorFormType();
$form->setData($paginator);
if ($form->isValid()) {
// ...
}
}
}
}
You can easily embed the form in your view like this:
{{ include 'AcmeDemoBundle:Form:paginator.html.twig' with { 'paginator': paginator } }}
Basically you just use the Form-component in your controller for validation purposes. If you want to set some default values or add additional arguments you might want to create a macro from that template, but for your use case this should suffice. Maybe someone else has a better solution but this is how I went with a similar problem in one of my projects.
another option is user the render twig helper. That way is is possible render the same form in the page as many time as you want. A difference is that using this helper, is also necessary to treat the form renderization as an independent controller Action namely:
in every place in your twig template you want to render the form in the helper to invoke the form there's must be something like this:
{{ render(controller('BundleNameBundle:Controller:Action', {
'param': paramId
})) }}
Thereafter is just a matter of creating the controller...
Another option is in the controller to create 2 forms:
//first form
$form = $this->createForm(new MyFormType(), ...);
//second form: must have different form name that default,
//to render in twig the fields with different ids
$formType = new MyFormType();
$formType->setName('second_form_name');
$formSecond = $this->createForm($formType, ...);
Send both when rendering the twig form:
return $this->render( ...
'form' => $form->createView(), 'formSecond'=>$formSecond->createView()));
Then define the second with name as
formSecond
, and it will conflict with the first.