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

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>

Related

(Symfony 4) Adding the filepond class wipes out my file widget and nothing shows up on the back end

I want to use filepond to make my input forms fancier, but it replaces my file widget with a series of elements of which have no name attribute, so nothing shows up on the back end.
Here is my Symfony form class:
class UserProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class)
->add('lastName', TextType::class)
etc...
// This is the widget I am concerned about for this question
->add('idFile', FileType::class, [
'label' => 'File or Files',
'required' => false,
'mapped' => false,
'multiple' => false
])
etc...
->add('save', SubmitType::class, [
'attr' => ['class' => 'save'],
]);
}
}
Here is what it looks like when I create the file widget in twig:
<div class="col-12">
<label for="file-upload" class="mb-2">Photo I.D.</label>
<!-- so right here where I add the filepond class -->
{{ form_widget(form.idFile, { 'id': 'file-upload', 'attr': {'class': 'form-control filepond'} } ) }}
</div>
Here are the filepond libraries I used, included at the bottom of my twig file:
<!-- Load FilePond library -->
<script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
<!-- <script src="https://unpkg.com/jquery-filepond/filepond.jquery.js">
</script> -->
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
So when the twig file loads, filepond takes over and converts my widget to a series of elements with all kinds of properties to them.
That's fine, I understand that filepond does this to fancy up your forms. But the problem is that on the back-end, nothing shows up.
One more thing to add - none of the elements which filepond creates have a "name" attribute to them, I don't understand why.
So when I look at the files suplerglobal on the back end, my idFile is not there.
Just to re-affirm, when I take out the filepond class, my idFile shows back up in the files suplerglobal at the back-end just fine.
Here is the twig-generated input element:
<div class="col-12">
<label for="file-upload" class="mb-2">Photo I.D.</label>
<input type="file" id="file-upload" name="user_profile[idFile]" class="form-control filepond" />
</div>
If I do an inspect element, among the filepond-generated divs there is a < input type="hidden" name="user_profile[idFile]" value="">
Here is the controller code at the back end:
public function saveProfileCompleteAction(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(UserProfileType::class, $user);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$idFile = $form['idFile']->getData();
If I do a dump($idFile); then it just displays null, and I have checked and made sure that I have selected a file to upload.
How can I use the filepond library and get my file input widget to show up on the back-end?

How do you treat the data if the select data is large amount at the time of registration and edit in Symfony2?

My\ConductBundle\Entity\Conduct:
// ...
manyToOne:
model:
targetEntity: My\ModelBundle\Entity\Model
inversedBy : conducts
joinColumn :
name : model
referencedColumnName: id
My\ModelBundle\Entity\Model:
// ...
oneToMany:
conducts:
targetEntity: My\ConductBundle\Entity\Conduct
mappedBy : model
cascade : ["persist", "remove"]
My\ConductBundle\Form\Type
class ConductType extends AbstractType
{
// ...
$builder->add('model', 'entity', array(
'class' => 'My\ModelBundle\Entity\Model',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('m')->where('m.disabled = 0')->orderBy('m.name', 'ASC');
},
'property' => 'name',
'empty_value' => '- choose an model option -',
'required' => false,
));
My\ConductBundle\Resources\views\registerForm.html.twig:
<ul data-prototype="
// ...
<div class='model'>
{{ form_widget(form.conducts.vars.prototype.model) | e }}
</div>
// ... ">
{% for conduct in form.conducts %}
<li>
// ...
// ... form error fields
// ... conduct fields
<div class='model'>
{{ form_widget(conduct.model) }}
</div>
</li>
{% endfor %}
</ul>
<button name='conduct-field-add-button'></button>
It works so far.
But it is possible that the data will be a large amount
because the data ('model' in this condition) can be registered by any user.
It is possible to be one million records or more.
So I have to consider to solution.
one step:
When there are few characters for search and there is data in large amount, I am surmising that processing becomes heavy. I want to avoid the way that is likely to be heavy process.
So I want to avoid this autocomplete solution.
two step:
I tried to other way that render trigger button for search models.
By clicking the trigger button, a dialog to select the model will be displayed.
And it will set the value of hidden field when select the model (id).
$builder->add('model', 'hidden', array(
'property_path' => 'model.id',
));
<input type='hidden' name='my_conduct_form_type[conducts][0][model]' value='5' />
However, in this case, I do not have the ability to combine data and form-type eventually.
It will be error.
Neither the property 'id' nor one of the methods 'setId()', '__set()' or '__call()' exist
and have public access in class 'My\ModelBundle\Entity\Model'. (500 Internal Server Error)
How do you treat the data if the select data is large amount at the time of registration and edit?

Invalid CSRF using symfony2 form type

I'm creating 2 forms in one action and these forms are submitted by jquery ajax to other 2 actions. Now, problem is - only first form works. Edit form throws that csrf token is invalid. Why is that happening? My code:
Creating forms:
$project = new Project();
$addProjectForm = $this->createForm(new AddProjectType(), $project, [
'action' => $this->generateUrl('tfpt_portfolio_versionhistory_addproject'),
'method' => 'POST',
'attr' => ['id' => 'newProjectForm']
]);
$editProjectForm = $this->createForm(new EditProjectType(), $project, [
'action' => $this->generateUrl('tfpt_portfolio_versionhistory_editproject'),
'method' => 'POST',
'attr' => ['id' => 'editProjectForm']
]);
Handling submit edit form (but add form is pretty much identical):
$project = new Project();
$form = $this->createForm(new EditProjectType(), $project);
$form->handleRequest($request);
if($form->isValid()){
//handle form
}
}
The only diffrence between these 2 forms is that edit form have one more field - hidden id. Both are submitted by jquery like that:
var form = $("#editProjectForm")
if(form.valid()){
$("#loader").show();
$.ajax({
type: form.attr('method'),
url: form.attr('action'),
data: form.serialize()
}).done(function(data){
//result
}
});
And i display forms like that:
{{ form_start(editProjectForm) }}
{{ form_errors(editProjectForm) }}
{{ form_widget(editProjectForm.name) }}
{{ form_widget(editProjectForm.id) }}
{{ form_rest(editProjectForm) }}
{{ form_end(editProjectForm) }}
Can somebody point my mistake? Isn't it possible to embed 3 forms in one action? Or i have to generate CSRF other way?
#Edit: I updated symfony to the newest release and now it's working prefect. Seems like this version had a bug or i got some lack of vendors code. Anyway, problem resolved.
I think you have to create two tokens in the controller:
$token_add = $this->get('form.csrf_provider')->generateCsrfToken('add');
$token_edit = $this->get('form.csrf_provider')->generateCsrfToken('edit');
and put in the view in hidden field. And then validate in the controller action that proccess the form
# Here you can validate the 'add' or 'edit' token
if (!$this->get('form.csrf_provider')->isCsrfTokenValid('add', $token)) {
$respuesta = array('mensaje' => 'Oops! Invalid token.',
'token' => $token);
return new Response(json_encode($respuesta));
}

Why getMethod() returns "GET" instead of "POST"?

The goal is to submit a POST form with two radiobuttons (tipo) and a text field (numero) to make a query in my DB and show the data to the user.
I am trying to submit the form below, however when I submit the form, the request coming accross is a 'GET REQUEST'. The form is in "SupuestoConfig.html.twig":
<div id="cuadro">
<form id="configurador" action="{{ path('configsup') }}" method="POST">
<p class="titulo_configurador">Elija supuesto penal:</p>
{{ form_row(form.tipo) }}
{{ form_row(form.numero, { 'label' : ' ', 'attr' : { 'class' : 'rec3' }}) }}
{{ form_rest(form) }}
<input type="submit" name="cargar" value="Cargar" class="inputbt"/>
</form>
</div>
I render the previous form in "principal.html.twig":
{{ render(controller('PprsBundle:Default:SupuestoConfig'), {'strategy': 'inline'}) }}
My "Controller.php":
/**
* #Route("/pprs/principal/supuesto={numero_supuesto}", name="configsup")
* #Template("PprsBundle:Default:SupuestoConfig.html.twig")
*/
public function SupuestoConfigAction($numero_supuesto = null)
{
$form = $this->createFormBuilder(null)
->add('tipo', 'choice', array(
'choices' => array(
'aleatorio' => 'Aleatorio',
'pornumero' => 'Por número'),
'multiple' => false,
'expanded' => true,
'data' => 'aleatorio'
))
// This add may contains error
->add('numero', 'text', array('label' => ' ','disabled' => true))
->getForm();
$peticion = $this->getRequest();
echo ('<script type="text/javascript">alert ("'.$peticion->getMethod().'");</script>');// Returns 'GET'
if ($peticion->isMethod('POST')) {
// Symfony2.2
$form->bind($peticion);
**$datos = $form->getData();**
*//foreach(array_keys($datos) as $p) {
//echo ('<script type="text/javascript">alert ("'.$datos.'");</script>');
//}*
if ($form->isValid()) { ... }
In Controller.php, despite I´ve got a GET request (when I remove the line
->add('numero', 'text',..
I´ve got a POST request, why is that?), in getData I don´t get the text field.
Finally, my routing.yml:
pprs_principal:
pattern: /pprs/principal/supuesto={numero_supuesto}/
defaults: { _controller: PprsBundle:Default:principal, numero_supuesto: 1 }
_pprs_principal:
pattern: /pprs/principal/
defaults: { _controller: FrameworkBundle:Redirect:redirect, route: pprs_principal }
Sorry for my bad english, Thanks in advance
Edit:
1) Anybody knows why I obtain a GET request instead of a POST when I add the text field in my createFormBuilder?
2) Anybody knows why Don't I get the text field when I call getData?
Help me please...
Maybe this answer could help you out:
getRequest() returns "GET" when posting form
Basically, when rendering a form with a {% render %} tag, it actually creates "another" request... It doesn't pass in the locale, the method, etc.
I opened a bug about this, and it went as By Design:
https://github.com/symfony/symfony/issues/7551

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