How to get the added item data from embedded collection in symfony 4 form ? allow_add seems not to work

Viewed 352

I have done a form with an entity (Crop Production) and its children items (the events) embedded in it, similar to the one we can find here i.e. with jQuery doing the work to add/remove item dynamically.

Everything works, the edition/deletion of the children entity, except that a added item in the form is not saved in the request.

It seems that as I add new item in the template with jQuery, the form prototype/tags are not recognized/mapped/linked with the form, but I can't figure out why as the prototype is the one automatically generated by symfony, i.e. the names of the dynamically added inputs are the good ones.

edit.html.twig (the template with jQuery)

{% extends picture is not defined ? 'base.html.twig' : 'blank.html.twig' %}




{% block main %}

<div class="card card-body">
{{ form_start(form) }}
     {{ form_row(form.Crop_production) }}
     {# render each row and field in the embedded form, in this example it's rendering the experience (Exp) fiels as rows #}
</div>
     <div class="col-sm-12 col-sm-offset-2">

<legend class="col-form-label required mt-3">Crop production events</legend>

       <div class="row" id="event_list" data-prototype="{{ form_widget(form.cropProductionEvents.vars.prototype)|e }}">

         {% for row in form.cropProductionEvents %}
             <div class="panel panel-warning">
             <div class="col-md-12 col-xl-12">
                 <div class="card mb-3 mt-3 widget-content">
                 <div class="widget-content-outer">
                     {{ form_row(row) }}
                 </div>
                 </div>
             </div>
             </div>

         {% endfor %}
       </div>
     </div>
 {{ form_end(form) }}
<br>


<script>
// this variable is the list in the dom, it's initiliazed when the document is ready
console.log("init");
var $collectionHolder;
// the link which we click on to add new items
var $addNewItem = $('<a href="#" class="btn btn-info mb-3 ml-3">Add new event</a>');
// when the page is loaded and ready
$(document).ready(function () {
 console.log("ready");

   // get the collectionHolder, initilize the var by getting the list;
   $collectionHolder = $('#event_list');
   // append the add new item link to the collectionHolder
   $collectionHolder.parent().append($addNewItem);
   // add an index property to the collectionHolder which helps track the count of forms we have in the list
   $collectionHolder.data('index', $collectionHolder.find('.panel').length)
   // finds all the panels in the list and foreach one of them we add a remove button to it
   // add remove button to existing items
   $collectionHolder.find('.panel').each(function () {
       // $(this) means the current panel that we are at
       // which means we pass the panel to the addRemoveButton function
       // inside the function we create a footer and remove link and append them to the panel
       // more informations in the function inside
       addRemoveButton($(this));
   });
   // handle the click event for addNewItem
   $addNewItem.click(function (e) {
       // preventDefault() is your  homework if you don't know what it is
       // also look up preventPropagation both are usefull
       e.preventDefault();
       // create a new form and append it to the collectionHolder
       // and by form we mean a new panel which contains the form
       addNewForm();
   })
});
/*
* creates a new form and appends it to the collectionHolder
*/
function addNewForm() {
   console.log("add new form");

   // getting the prototype
   // the prototype is the form itself, plain html
   var prototype = $collectionHolder.data('prototype');
   // get the index
   // this is the index we set when the document was ready, look above for more info
   var index = $collectionHolder.data('index');
   // create the form
   var newForm = prototype;
   // replace the __name__ string in the html using a regular expression with the index value
   newForm = newForm.replace(/__name__/g, index);
   // incrementing the index data and setting it again to the collectionHolder
   $collectionHolder.data('index', index+1);
   // create the panel
   // this is the panel that will be appending to the collectionHolder
   var $panel = $('<div class="panel panel-warning"></div>');
   // create the panel-body and append the form to it
   var $panelBody = $('<div class="col-md-12 col-xl-12"></div>').append($('<div class="card mb-3 mt-3 widget-content"></div>').append($('<div class="widget-content-outer"></div>').append($('<fieldset class="form-group"></fieldset>').append(newForm))));
   // append the body to the panel
   $panel.append($panelBody);
   // append the removebutton to the new panel
   addRemoveButton($panel);
   // append the panel to the addNewItem
   // we are doing it this way to that the link is always at the bottom of the collectionHolder
   $panel.hide();
   $("#event_list").append($panel);
   $panel.slideDown(200);

   console.log("selection holder");
   console.log($collectionHolder.data('index'));
}

/**
* adds a remove button to the panel that is passed in the parameter
* @param $panel
*/
function addRemoveButton ($panel) {
   console.log("add remove button");

   // create remove button
   var $removeButton = $('<a href="#" class="btn btn-danger">Remove</a>');
   // appending the removebutton to the panel footer
   var $panelFooter = $('<div class="panel-footer"></div>').append($removeButton);
   // handle the click event of the remove button
   $removeButton.click(function (e) {
       e.preventDefault();
       // gets the parent of the button that we clicked on "the panel" and animates it
       // after the animation is done the element (the panel) is removed from the html
       $(e.target).parents('.panel').slideUp(200, function () {
           $(this).remove();
       })
   });
   // append the footer to the panel
   var $form = $panel.find(".widget-content-outer");
   console.log($form);
   $form.append($panelFooter);
}
</script>

{% endblock %}

CropProductionType.php (the parent form type)

<?php

namespace App\Form;

use App\Entity\CropProduction;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type as Type;

class CropProductionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

      $units = [
        "money" => [
          'currency' => 'USD',
          'help' => 'value in $',
          'required' => false,
          "attr" => ["step" => '0.01']
        ],
        "surface" => [
          'help' => 'value in ha',
          "attr" => ["step" => '0.01']
        ]
      ];

     $builder->add(
        $builder->create('Crop_production', Type\FormType::class, [
                  'inherit_data' => true,
                  'row_attr' => [
                    'class' => 'form-col-2'
                  ],
                ])
                ->add('type', Type\ChoiceType::class, ["label" => "Type harvested", "required" => true, "choices" => [
                      "Grain" => "Grain",
                      "Fruit" => "Fruit",
                      "Tuber" => "Tuber",
                      "Stem" => "Stem"
                    ]
                  ])

                  ->add('dateHarvested', Type\DateType::class, [
                      'label' => 'Date Harvested',
                      'widget' => 'single_text',
                      // this is actually the default format for single_text
                      'format' => 'yyyy-MM-dd',
                  ])

                ->add('prodFarmerStatement', Type\NumberType::class, ["label" => "Production farmer statement", 'help' => 'in kg dry/ha (grain) or in ton fresh/ha (other)'])

                ->add('prodFieldMeasure', Type\NumberType::class, ["label" => "Production field measure", 'help' => 'in kg dry/ha (grain) or in ton fresh/ha (other)'])
      );



        $builder->add('cropProductionEvents', Type\CollectionType::class, [
                   'entry_type' => CropProductionEventType::class,
                   'entry_options' => [
                       'label' => false
                   ],
                   // this allows the creation of new forms and the prototype too
                   'allow_add' => true,
                   // self explanatory, this one allows the form to be removed
                   'allow_delete' => true,
                   'delete_empty' => true,
                   'prototype' => true,
                   'by_reference' => false,

               ]);




      $builder->add('save', Type\SubmitType::class, ['label' => 'Save']);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => CropProduction::class,
            'label' => 'fdg',
        ]);
    }


}

CropProductionController.php edit function (the parent controller)

  /**
     * @Route("{id}/delete", name="_delete")
     */
    public function delete_production($id)
    {
      $em = $this->getDoctrine()->getManager();
      $crop_production = $em->getRepository(CropProduction::class)->find($id);
      $crop = $crop_production->getCrop();
      $em->remove($crop_production);
      $em->flush();
      return $this->redirectToRoute("crop_details", ["id" => $crop->getId(), '_fragment' => 'crop_production']);
    }

    /**
     * @Route("{id}/edit", name="_edit")
     */
    public function edit(Request $request, $id)
    {
      $em = $this->getDoctrine()->getManager();
      $form = NULL;

      $crop_production = $em->getRepository(CropProduction::class)->find($id);

      // $events = new ArrayCollection();
      // foreach ($crop_production->getCropProductionEvents() as $event) {
      //   $events->add($event);
      // }

      if(!$crop_production) return $this->render('error/blank.html.twig', []);
      $form = $this->createForm(CropProductionType::class, $crop_production);

      $form->handleRequest($request);
      if ($form->isSubmitted() && $form->isValid()) {

          

          $em->persist($crop_production);
          $em->flush();
          return $this->redirectToRoute("crop_details", ["id" => $crop_production->getCrop()->getId()]);
      }



      return $this->render('crop_production/edit.html.twig', [
        "crop_prod" => $crop_production,
        'form' => $form->createView(),
      ]);
    }

Any idea of what I could miss to get the added elements on the template ? Thank you

1 Answers

There is no data-index="" on your <div class="row" id="event_list" >. You have to put current amount of children (types of collection) so addNewForm() could count on...

Like this:

<div class="row" id="event_list" data-index="{{ form.cropProductionEvents|length }}" data-prototype="{{ form_widget(form.cropProductionEvents.vars.prototype)|e }}">
Related