How to Embed AJAX(!) Forms in Symfony 1.2 Admin Generator

4 05 2009

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)

symfony embedded forms ajax

symfony embedded forms ajax

edit form example

edit form example