Based on a few informative articles, helping a veteran Symfony 1.0 programmer navigate through the 1.2 form structure, I was able to embed forms in an admin generated form based on a one-to-many relationship. See those articles at:
How to Embed Forms in Symfony 1.2 Admin Generator
Interactive embedded forms
For the sake of speed, though, I wanted to be able to add more embedded forms dynamically via AJAX before submitting the whole form. Here’s how I did it:
Adding the initial forms; on edit, load the related objects with a delete button; on new object or edit, add 3 blank embedded forms to start with.
// lib/forms/PoForm.class.php
public function configure() {
sfLoader::loadHelpers(array('jQuery','Asset','Tag','Url'));
$index = 0;
foreach ($this->getObject()->getItemPos() as $book){
$this->embedForm('item_pos'.++$index, new ItemPoForm($book));
$label = "Item $index".jq_link_to_remote(image_tag('/sf/sf_admin/images/delete'), array(
'url' => 'po/deleteItem?idd='.$book->getId(),
'success' => "jQuery('.sf_admin_form_field_item_pos$index').remove();",
'confirm' => 'Sure???',
));
$this->widgetSchema->setLabel('item_pos'.$index,$label);
}
$a=sfContext::getInstance()->getUser()->getAttribute('N1added'.$this->getObject()->getId());
$more = $this->getObject()->isNew()?max(3,$a-$index):($a>($index+3)?$a-$index:3);
for($i=0;$i<$more;$i++){
$ip = new ItemPo();
$ip->setPo($this->getObject());
$this->embedForm('item_pos'.++$index, new ItemPoForm($ip));
$this->widgetSchema->setLabel('item_pos'.$index,"Item $index");
}
$label = "Item $index".jq_link_to_remote(image_tag('/sf/sf_admin/images/add'), array(
'url' => 'po/addItems?po='.$this->getObject()->getId().'&index='.$index,
'update' => 'sf_fieldset_none',
'position'=> 'bottom',
));
$this->widgetSchema->setLabel('item_pos'.$index,$label);
sfContext::getInstance()->getUser()->setAttribute('N1added'.$this->getObject()->getId(), $index);
}
The last item on our list gets an “add” image, which makes an AJAX call to add new blank forms.
You can see we also use a user attribute to store how many embedded forms are being displayed, so we know what index we’re up to when we add more forms, and we know how many forms to regenerate on error.
Let’s create the add and delete actions-
// apps/frontend/modules/po/actions/actions.class.php public function executeDeleteItem(sfWebRequest $request) { $sub_category = ItemPoPeer::retrieveByPK($request->getParameter('idd')); if (!$sub_category) {return $this->renderText('Error '.$request->getParameter('idd'));} $sub_category->delete(); if (!$this->getRequest()->isXmlHttpRequest()){ $this->redirect('@po_edit?id='.$sub_category->getPo()->getId()); } return $this->renderText(''); } public function executeAddItems(sfWebRequest $request){ $po = $request->getParameter('po'); $index = sfContext::getInstance()->getUser()->getAttribute('N1added'.$po); sfLoader::loadHelpers(array('jQuery','Asset','Tag','Url')); $form = new PoForm(); $form->setWidgets(array('id' => new sfWidgetFormInputHidden(),)); $form->setValidators(array('id' => new sfValidatorPropelChoice(array('model' => 'Po', 'column' => 'id', 'required' => false)),)); $widgetSchema = $form->getWidgetSchema(); $widgetSchema->setNameFormat('po[%s]'); for($i=0;$i<3;$i++){ $ip = new ItemPo(); if($po) $ip->setPoId($po); $form->embedForm('item_pos'.++$index, new ItemPoForm($ip)); $widgetSchema->setLabel('item_pos'.$index,"Item $index"); } $label = "Item $index".jq_link_to_remote(image_tag('/sf/sf_admin/images/add'), array( 'url' => 'po/addItems?po='.$po.'&index='.$index, 'update' => 'sf_fieldset_none', 'position'=> 'bottom', )); $widgetSchema->setLabel('item_pos'.$index,$label); sfContext::getInstance()->getUser()->setAttribute('N1added'.$po, $index); $this->form = $form; return $this->renderPartial('po/items'); }
The po/items partial is just a snippet from the auto-generated _form.php-
<?php foreach($configuration->getFormFields($form, 'new') as $fields): ?> <?php foreach ($fields as $name => $field): ?> <?php if ((isset($form[$name]) && $form[$name]->isHidden()) || (!isset($form[$name]) && $field->isReal())) continue ?> <?php include_partial('po/form_field', array( 'name' => $name, 'attributes' => $field->getConfig('attributes', array()), 'label' => $field->getConfig('label'), 'help' => $field->getConfig('help'), 'form' => $form, 'field' => $field, 'class' => 'sf_admin_form_row sf_admin_'.strtolower($field->getType()).' sf_admin_form_field_'.$name, )) ?> <?php endforeach; ?> <?php endforeach; ?>
Finally, we modify our form’s bind method to remove unused embedded forms, and link the rest to our main object-
// lib/forms/PoForm.class.php public function bind(array $taintedValues = null, array $taintedFiles = null) { for($i=1;$i<=sfContext::getInstance()->getUser()->getAttribute('N1added'.$this->getObject()->getId(),3);$i++) { if(!isset($taintedValues["item_pos$i"]) || empty($taintedValues["item_pos$i"]['item_id'])) { $this->embeddedForms['item_pos'.$i]->getObject()->setDeleted(true); unset($this->embeddedForms["item_pos$i"],$this->validatorSchema["item_pos$i"]); unset($taintedValues['item_pos'.$i]); } else { $this->embeddedForms['item_pos'.$i]->getObject()->setPo($this->getObject()); } } return parent::bind($taintedValues,$taintedFiles); }
That should do it!
(In case you’re having trouble seeing the code, you can go here to see it all)