skip to content

Dynamic embedded forms in Symfony

Let's see Symfony 1.2 forms in depth to make them do what we want

Embedded forms in Symfony are a very useful tool, but ¿how to add them dynamically from the view using AJAX?

Simple form

Let’s take a simple applicaton. There will be flashcards with a question and its answer. The form for creating these cards could be this:

original form

With an embedded form

We may want to offer an image as a hint, embedding a PictureForm:

embedded form

class CardForm extends BaseCardForm
{
  public function configure()
  {
    unset($this['cardset_id'],$this['usercard_list']);
    $this->embedForm('picture', new PictureForm());
  }
}

Dynamic embedded forms

¿What if we are interested in having more than one image as hint, a variable number of them?

dynamic form

In this case we need to write more code. Let’s start with the form (CardForm.class.php in my case):

class CardForm extends BaseCardForm
{
  public function configure()
  {
    unset($this['cardset_id'],$this['usercard_list']);

    //Embedding at least a form
    $pictures = $this->getObject()->getPictures();
    if (!$pictures){
      $picture = new Picture();
      $picture->setCard($this->getObject());
      $pictures = array($picture);
    }

    //An empty form will act as a container for all the pictures
    $pictures_forms = new SfForm();
    $count = 0;
    foreach ($pictures as $picture) {
      $pic_form = new PictureForm($picture);
      //Embedding each form in the container
      $pictures_forms->embedForm($count, $pic_form);
      $count ++;
    }
    //Embedding the container in the main form
    $this->embedForm('pictures', $pictures_forms);
  }
}

This code will be valid for new objects (we will embed only a form to start with) and for objects with already embedded pictures.

Now we will write an AddPicture() function that will be very useful:

public function addPicture($num){
  $pic = new Picture();
  $pic->setCard($this->getObject());
  $pic_form = new PictureForm($pic);

  //Embedding the new picture in the container
  $this->embeddedForms['pictures']->embedForm($num, $pic_form);
  //Re-embedding the container
  $this->embedForm('pictures', $this->embeddedForms['pictures']);
}

This function adds a PictureForm to the main form. This is useful for two reasons (we will see this in more detail later):

  1. When adding a picture from an AJAX call, we will call an action that will run this function to obtain a form with the embedded form . Them, that action will render only what we need. And that will be what we will inject in the view.
  2. When we bind this form, we need the form that is going to be bound to the user-written data to have the right number of embedded pictures. That is, if we are going to bind a user-introduced form with a given number of embedded pictures, the form object must have the same number of embedded forms. For that reason, we will overload the bind function to add as many embedded pictures as we need.

Step by step. First: in the view, we will add this AJAX function. I use Jquery, it is not difficult to write the same thing in Prototype:

<script type="text/javascript">
var pics = <?php print_r($form['pictures']->count())?>;

function addPic(num) {
  var r = $.ajax({
    type: 'GET',
    url: '<?php echo url_for('card/addPicForm')?>'+'<?php echo   ($form->getObject()->isNew()?'':'?id='.$form->getObject()->getId()).($form->getObject()->isNew()?'?num=':'&num=')?>'+num,
    async: false
  }).responseText;
  return r;
}
$().ready(function() {
  $('button#add_picture').click(function() {
    $("#extrapictures").append(addPic(pics));
    pics = pics + 1;
  });
});
</script>

And after the form a button to add forms and a place to inject them after the AJAX call:

<?php echo $form ?>
<div id="extrapictures"/>

<tr><td><div><button id="add_picture" type="button"><?php echo "Añadir otra imagen"?></button></div></td></tr>

In the action, we write the this executeAddPicForm() function:

public function executeAddPicForm($request)
{
  $this->forward404unless($request->isXmlHttpRequest());
  $number = intval($request->getParameter("num"));

  if($card = CardPeer::retrieveByPk($request->getParameter('id'))){
    $form = new CardForm($card);
  }else{
    $form = new CardForm(null);
  }

  $form->addPicture($number);

  return $this->renderPartial('addPic',array('form' => $form, 'num' => $number));
}

Basically, this function creates a form and calls to addPicture() to have a new form with an embedded form withh the right number. Then we run a partial addPic with this form and the number of embedded form to show. This partial has the following code:

<?php echo $form['pictures'][$num]['id']->render()?>
<?php echo $form['pictures'][$num]['file']->renderRow();?>

With this we have a button that adds new subforms. But, what about saving? As we said before, we will need bid to do part of the task. Its new code will be:

public function bind(array $taintedValues = null, array $taintedFiles = null)
{
  foreach($taintedValues['pictures'] as $key=>$newPic)
  {
    if (!isset($this['pictures'][$key]))
    {
      $this->addPicture($key);
    }
  }
  parent::bind($taintedValues, $taintedFiles);
}

And with this change we can use this form as a normal one.

PS: I must give people credit about this one. I looked at several places for this info and this post was very useful. It is basically the same, but more focused in adding fields synamically instead of embedded forms.

PS2: See also this post and this one about this topic.