Dynamic embedded forms in symfony
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:

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

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?

In this case we need to write more code. Let’s start with the form (CardForm.class.php en 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 bounded 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.
I’ve just finished writing up a similar tutorial, but I took a bit of a different approach. If you’re interested, check out http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ and let me know what you think, I would love to hear your opinion.
Thank you.. your post has inspired me..
Thank you, very helpfull.
I wanted to thank you for your tutorial, it’s been very helpful for me.
I have a simple question, but I’m struggling. I would like to put javascript code to a separate file, in /web/js directory, so in view I have only use_javascript(‘myfile.js’) and no javascript code. How should I modify your example? And what are the reasons to keep js code explicitly in view file?
Hi, Goshka. I am glad you found this helpful :) There is no reason to keep javascript in the view except for simiplicity. You can put it in a separate javascript. Just make sure you give a value to the variable holding the number of pictures.
var pics = < ?php print_r($form['pictures']->count())?>;
i dont understand this ligne of code
CardPeer::retrieveByPk($request->getParameter(‘id’)))
Hey fedora!
You can gonna check on developpez.com over there :
http://www.developpez.net/forums/d899403/php/bibliotheques-frameworks/symfony/formulaire-dynamique-ajax/#post5099275
I hop it will help you!
Hi Nacho, thanks for your post, just one thing: could you give the yaml schema file that you’re using? In particular for the relations between the two tables…
@fedora: If we are editing, an id for the card that we are editing will be passed as a parameter, Then with that line (using Propel) we will retrieve the card object. In Doctrine is different: $article = Doctrine::getTable(‘Card’)->find($request->getParameter(‘id’));
Check post on embedded forms here using symfony 1.4 and doctrine…
Many thanks for your tuto.
I’have a question : if main form’s validation fails, my dynamicaly embedded forms don’t appear when the form is displayed again.
How would you deal with this ?
@rastaferraille
They should appear. The bind function is calling $this->addPicture($key); for each picture submitted.
Oh sorry ! I’d just forgotten to display them in my view ! shame on me