Symfony Form Builder: How to dynamically add Fields to a Poll - symfony

I created a Symfony Poll-Bundle which has the Entitys Campaign->Block->Line->Field->PollResult.
So i have a CollectionType CampaignType which consists of many blocks.
One block consist of many Lines.
One Line consist of many Fields.
(One Line is for example the line for fever and the fields are for intensity, and the period from and due the fever occured)
And every Field has one PollResult which holds the Answer of the user who filled out the campaign.
Now i want to make the user to be able to add new Fields to a line to enter a second fever period for example.
So i need a 'Add a Line'-Button for every Line that duplicates this line.
I used this documentation to do that.
First i added 'allow_add' => true to my LineType FormBuilder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('userAddFlag');
$builder->add('fields', CollectionType::class, array(
'entry_type' => FieldType::class,
'entry_options' => array('label' => false),
'allow_add' => true,
));
}
Then i added this to my view with the data-prototype:
<ul class="fields" data-prototype=" {{ form_widget(line.fields.vars.prototype)|e('html_attr') }} ">
<li>
{% for field in line.fields %}
<div>
{% for pollResult in field.pollResults %}
<div class="formLabelDiv">{{ field.vars.value.getTranslationName(app.request.getLocale()) }}</div>
<div class="formWidgetDiv">{{ form_widget(pollResult) }}</div>
{% endfor %}
</div>
{% endfor %}
</li>
</ul>
And last i added this jQuery code:
<script>
var $collectionHolder;
//setup an "add a Line" Link
var $addLineButton = $('<button type="button" class="add_line_link">Add a Line</button>');
var $newLinkLi = $('<li></li>').append($addLineButton);
jQuery(document).ready(function (){
//Get the ul that holds the collection of fields
$collectionHolder = $('ul.fields');
//add the "add a line" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
//count the current form inputs we have, use that as the new index when inserting a new item
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addLineButton.on('click', function (e){
addFieldsForm($collectionHolder, $newLinkLi);
})
})
function addFieldsForm($collectionHolder, $newLinkLi){
//get the data-prototyp
var prototype = $collectionHolder.data('prototype');
//get the new index
var index = $collectionHolder.data('index');
var newForm = prototype;
$collectionHolder.data('index', index+1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
</script>
So now the 'Add a Line'-Button appears on the page, but it doesn't add anything..
How can i add a new field and how am i able to duplicate the whole line and not only add one Field to the added line (I want to have the three fields intensity(dropdown), from and due(both textfield with datepicker))

Try adding 'by_reference' => false, to the LineType.php

Related

Symfony 6: embedded form collection, add only new elements and omit existing elements

I have a FormType which contains an embedded collection of another FormType, all is working properly and done according to How to Embed a Collection of Forms.
My embedded FormType is an attachment list, which has title and file upload.
AttachmentType:
builder
->add('title', null, ['label' => 'Title', 'required' => true])
->add('attachmentFile', FileType::class, [
'label' => 'File',
'mapped' => false,
'required' => true,
'constraints' => [
new File([
'maxSize' => '1024k',
])
],
])
The problem is, when editing the parent object, I want to allow new attachments but I don't want to edit previous attachments, as it would be forcing me to upload the same files every time I edit the parent object.
All elements and javascript are done according to the docs linked, I don't find any property for CollectionType that would allow add but wouldn't allow edit or something similar. The javascript seems too dependent on external stuff to allow some modification.
Javascript
// assets/controllers/form-collection_controller.js
import { Controller } from '#hotwired/stimulus';
export default class extends Controller {
static targets = ["collectionContainer"]
static values = {
index : Number,
prototype: String,
}
addCollectionElement(event)
{
const item = document.createElement('li');
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
this.collectionContainerTarget.appendChild(item);
this.indexValue++;
}
}
Twig template
<div {{ stimulus_controller('form-collection') }}
data-form-collection-index-value="{{ form.attachments|length > 0 ? form.attachments|last.vars.name + 1 : 0 }}"
data-form-collection-prototype-value="{{ form_widget(form.attachments.vars.prototype)|e('html_attr') }}"
>
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
<button type="button" class="btn btn-secondary" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Añadir adjunto</button>
</div>

Symfony 3 : How to Embed a Collection of Forms - after submit I only have one element in the array instead of many

Hello guys,
On Symfony 3 :
I tried to follow the requirements of "How to Embed a Collection of Forms" from Symfony 3 Documentation. It works for a defined list as they propose first. But when I try the next step: Allowing new Tags with the Prototype, it only returns my last embed Form.
So I know that my Entity works, aswell as the EntityType. The error must be on the Twig.
Thanks in advance for your help!
It works for a defined list as they propose in the first part.
So I know that my Entity works, aswell as the EntityType.
enter code here
{% extends "#App/baseAdmin.html.twig" %} .
{% block contenu %} .
{#{{ dump(formreservation) }}#} .
{#{{ form(formreservation) }}#} .
<div> .
Date : {{ "now"|date("d/m/Y") }} .
{{ form_start(formreservation, {'attr': {'class': 'form'}}) }} .
{#{{ dump(formreservation) }}#} .
<p> .
{#retourne message erreur si besoin après méthode isValid dans Controleur#} .
{{ form_errors(formreservation.spectacle) }} .
{{ form_label(formreservation.spectacle, null,
{'label_attr': {'class': 'form-label'}}) }} :
{{ form_widget(formreservation.spectacle, {'attr':
{'class': 'form-control'}}) }} .
</p>
<p>
{{ form_label(formreservation.spectateur, null,
{'label_attr': {'class': 'form-label'}}) }}
<ul class="spectateur" data-prototype="{{ form_widget(formreservation.spectateur.vars.prototype)|e('html_attr') }}">
</ul>
</p>
<p>
{#retourne message erreur si besoin après méthode isValid dans Controleur#}
{{ form_errors(formreservation.client) }}
{{ form_label(formreservation.client, null,
{'label_attr': {'class': 'form-label'}}) }} :
{{ form_widget(formreservation.client, {'attr':
{'class': 'form-control'}}) }}
</p>
{# génération du champ CSRF - _token# (Cross Site Request Forgeries en champ caché #}
{{ form_rest(formreservation) }}
{{ form_end(formreservation) }}
</div>
{# Partie JavaScript #}
<script
src="https://code.jquery.com/jquery-3.3.1.js"
integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60="
crossorigin="anonymous">
</script>
<script>
var $collectionHolder;
// setup an "add a tag" link
var $addTagButton = $('<button type="button" class="add_tag_link">Ajoutez un spectateur</button>');
var $newLinkLi = $('<li></li>').append($addTagButton);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
var $collectionHolder = $('ul.spectateur');
// add a delete link to all of the existing tag form li elements
//inutile pour le moment, ajoute un bouton qui créé la confusion
/* $collectionHolder.find('li').each(function() {
addTagFormDeleteLink($(this));
});*/
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addTagButton.on('click', function(e) {
// add a new tag form (see next code block)
e.preventDefault();
addTagForm($collectionHolder, $newLinkLi);
});
});
function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
//console.log($collectionHolder);
var prototype = $collectionHolder.data('prototype');
//console
// get the new index
var index = $collectionHolder.data('index');
var newForm = prototype;
// You need this only if you didn't set 'label' => false in your tags field in TaskType
// Replace '__name__label__' in the prototype's HTML to
// instead be a number based on how many items we have
newForm = newForm.replace(/__name__label__/g, 'Spectateur n° '+ index);
//newForm = newForm.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1 );
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
// add a delete link to the new form
addTagFormDeleteLink($newFormLi);
}
function addTagFormDeleteLink($tagFormLi) {
var $removeFormButton = $('<button type="button">enlever ce spectateur</button><br>');
$tagFormLi.append($removeFormButton);
$removeFormButton.on('click', function(e) {
// remove the li for the tag form
e.preventDefault();
$tagFormLi.remove();
});
}
</script>
{% endblock %}
Expected result : a collection containing all the embed Forms.
Actually it only returns the last element in the embed Form. I did check on the Entity and it doesn't use the "add" method that I did add to the entity.
here the add part of ReservationType builder:
->add('spectateurs', CollectionType::class, [
'entry_type' => SpectateurReservationType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
] .
) .
)
You just have commented a part of JS code
newForm = newForm.replace(/__name__/g, index);
Uncomment this and anything would be nice
my line was:
newForm = newForm.replace(/name__label/g, 'Spectateur n° '+ index);
My mistake was, I thought I could change this as a textlabel:
Great it only was this! many thanks, you save me! Now it works perfectly!

symfony 2 - twig - How view more (20) fields with use "prototype"?

I want view 20 same fields (name: matchday) with use "prototype".
I have a code like this:
Form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('matchday', 'collection', array(
'allow_add' => true,
'type' => new MatchdayType(),
))
...
}
and View (Twig):
<form action="{{ path('meet_create') }}" method="post" {{ form_enctype(form) }}>
{% for i in 1..20 %}
{{ form_widget(form.matchday.vars.prototype) }}
{% endfor %}
<p>
<button type="submit">Create</button>
</p>
</form>
but I don' know how use iteration in this code with "prototype" .
thanks
The prototype is only a model that you can use to create new fields in the form's collection, but it's not really a field per say. If you want to display 20 empty fields from the beginning, you have basically two choices:
add 20 empty 'matchday' objects in the form object in your controller before creating the form
use Javascript (jQuery) to add the fields in the view on the client side
The first solution would give something like this:
$meet = new Meet();
for ($i=0; $i < 20; $i++) {
$matchday = new MatchDay();
$meet->getMatchdays->add($matchday);
}
$form = $this->createForm(new MeetType(), $meet);
For the second solution, you have a good example in the Symfony Cookbook about form collection: http://symfony.com/doc/current/cookbook/form/form_collections.html

Grouped checkboxes in Symfony / twig

I have 2 entities: Projects and Categories. I have a ManyToMany relation between these two.
The Categories has ManytoOne relation with the entity "industry"
At this moment, there is no direct relation between Projects and industry and I would like to keep this like so, for further search functionality. So in the category table, the list includes categories from all industries.
When I build the form to edit the project (using the form widget), I have a list of checkboxes representing all the categories listed in my category table.
I would like to group the category choices by industry. How can this be done on the form layout only? How can I extract the industry value from the twig widget form data and group the checkboxes by the industry entity?
Thanks Leevi,
I could not find how to implement the suggestion above using both industry and category related entities... I finally found this way of going around the issue, tell me if there is a simpler way, but this works perfect now.
This is my form in the controller
$form = $this->createFormBuilder($project)
->add('categories', 'entity', array(
'class' => 'ACMEProjectBundle:Category',
'property' => 'name',
'expanded' => true,
'multiple' => true,
->getForm();
I also pass to the rendered form the array of industries which has each a list of related categories
$industries = $this->getDoctrine()->getManager()->getRepository('ACMEProjectBundle:Industry')->findall();
In the form.html.twig template
{{ form_errors(form) }}
<form method="post" {{ form_enctype(form) }}>
{% for industry in industries %}
<h4>{{industry.name}}</h4>
<ul class="unstyled">
{% for category in industry.categories %}
{% set key = category.id %}
<li>{{ form_widget(form.categories[key]) }}{{category.name}}</li>
{% endfor %}
</ul>
{% endfor %}
{{form_rest(form)}}
Which gives me the wanted results.
Hopefully this will be enough direction without giving you exact code examples :).
You'll have to setup your form with an expanded, multiple, entity field like so:
<?php
// src/Acme/ProjectBundle/Controller/DefaultController.php
namespace Acme\ProjectBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\ProjectBundle\Entity\Project;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// create a project and give it some dummy data for this example
$project = new Project();
$form = $this->createFormBuilder($project)
->add('categories', 'entity', array(
'expanded' => true,
'multiple' => true,
'group_by' => 'industry.title'
))
->add('save', 'submit')
->getForm();
return $this->render('AcmeProjectBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
The group_by parameter groups the options based on the property path:
See: http://symfony.com/doc/current/reference/forms/types/entity.html#group-by
Now group_by renders a select tag but you should be able to override that with a custom twig theme or manually in the template.
Given the form above you can access the choices in {{ form.categories.vars.choices }} and iterate over them manually.
See: {% block choice_widget_collapsed %} in form_div_layout.html.twig to see how the select box is rendered.
Here's some more information of form theming: http://symfony.com/doc/current/cookbook/form/form_customization.html

collection Field Type not creating form elements

I'm trying to create a form which will add a new text box every time the 'Add new box' link got clicked.
I read through the following example.
http://symfony.com/doc/current/reference/forms/types/collection.html
Basically I was following the example from the book. But when the page is rendered and I click on the link nothing happens.
Any thoughts?
Thanks.
This is my controller.
public function createAction() {
$formBuilder = $this->createFormBuilder();
$formBuilder->add('emails', 'collection', array(
// each item in the array will be an "email" field
'type' => 'email',
'prototype' => true,
'allow_add' => true,
// these options are passed to each "email" type
'options' => array(
'required' => false,
'attr' => array('class' => 'email-box')
),
));
$form = $formBuilder->getForm();
return $this->render('AcmeRecordBundle:Form:create.html.twig', array(
'form' => $form->createView(),
));
}
This is the view.
<form action="..." method="POST" {{ form_enctype(form) }}>
{# store the prototype on the data-prototype attribute #}
<ul id="email-fields-list" data-prototype="{{ form_widget(form.emails.get('prototype')) | e }}">
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
Add another email
</form>
<script type="text/javascript">
// keep track of how many email fields have been rendered
var emailCount = '{{ form.emails | length }}';
jQuery(document).ready(function() {
jQuery('#add-another-email').click(function() {
var emailList = jQuery('#email-fields-list');
// grab the prototype template
var newWidget = emailList.attr('data-prototype');
// replace the "$$name$$" used in the id and name of the prototype
// with a number that's unique to our emails
// end name attribute looks like name="contact[emails][2]"
newWidget = newWidget.replace(/\$\$name\$\$/g, emailCount);
emailCount++;
// create a new list element and add it to our list
var newLi = jQuery('<li></li>').html(newWidget);
newLi.appendTo(jQuery('#email-fields-list'));
return false;
});
})
</script>
This problem can be solved by referring to the following link.
https://github.com/beberlei/AcmePizzaBundle
Here you will find the same functionality being implemented.
I've been through this too.
Answer and examples given to this question and the other question I found did not answer my problem either.
Here is how I did it, in some generic manner.
In generic, I mean, Any collection that I add to the form just need to follow the Form template loop (in a macro, for example) and that's all!
Using which convention
HTML is from Twitter Bootstrap 2.0.x
Javascript code is already in a $(document).ready();
Following Symfony 2.0.x tutorial
Using MopaBootstrapBundle
Form Type class
class OrderForm extends AbstractType
{
// ...
public function buildForm(FormBuilder $builder, array $options)
{
// ...
$builder
->add('sharingusers', 'collection', array(
'type' => new UserForm(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required'=> false
));
// ...
}
}
JavaScript
/* In the functions section out of document ready */
/**
* Add a new row in a form Collection
*
* Difference from source is that I use Bootstrap convention
* to get the part we are interrested in, the input tag itself and not
* create a new .collection-field block inside the original.
*
* Source: http://symfony.com/doc/current/cookbook/form/form_collections.html
*/
function addTagForm(collectionHolder, newBtn) {
var prototype = collectionHolder.attr('data-prototype');
var p = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
var newFormFromPrototype = $(p);
var buildup = newFormFromPrototype.find(".controls input");
var collectionField = $('<div class="collection-field"></div>').append(buildup);
newBtn.before(collectionField);
}
/* ********** */
$(document).ready(function(){
/* other initializations */
/**
* Form collection behavior
*
* Inspired, but refactored to be re-usable from Source defined below
*
* Source: http://symfony.com/doc/current/cookbook/form/form_collections.html
*/
var formCollectionObj = $('form .behavior-collection');
if(formCollectionObj.length >= 1){
console.log('run.js: document ready "form .behavior-collection" applied on '+formCollectionObj.length+' elements');
var addTagLink = $('<i class="icon-plus-sign"></i> Add');
var newBtn = $('<div class="collection-add"></div>').append(addTagLink);
formCollectionObj.append(newBtn);
addTagLink.on('click', function(e) {
e.preventDefault();
addTagForm(formCollectionObj, newBtn);
});
}
/* other initializations */
});
The form template
Trick here is that I would have had used the original {{ form_widget(form }} but I needed to add some specific to the view form and I could not make it shorter.
And I tried to edit only the targeted field and found out it was a bit complex
Here is how I did it:
{# All form elements prior to the targeted field #}
<div class="control-collection control-group">
<label class="control-label">{{ form_label(form.sharingusers) }}</label>
<div class="controls behavior-collection" data-prototype="{{ form_widget(form.sharingusers.get('prototype'))|escape }}">
{% for user in form.sharingusers %}
{{ form_row(user) }}
{% endfor %}
</div>
</div>
{{ form_rest(form) }}

Resources