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

Advertisements

Actions

Information

28 responses

5 05 2009
John

Thanks mate,

Really helpful tutorial – much more user friendly than having to save the form to add an extra item.

John

14 05 2009
leyan

hi

i need to do something like that in my project, i have a generated form with tags user, password and password_again and i want the passwords tags hidden until i click on change password and then bring it up with some ajax effect, no idea how to do it, any suggestions?

14 05 2009
israelwebdev

leyan, what you describe sounds nothing like what I’ve done here.
I think what you’re looking for is simply to use CSS display:none to hide the element when the page loads, and add an effect to “change password” to show the hidden fields – if you’re using jQuery, you can use the basic show function. (AJAX is not the correct term for this behavior.) Good luck.

1 06 2009
Stefan

Well done! In my opinion this tutorial is for advanced users only. More details abaout generating the module or the schema.yml or the templates would have been nice.

Furthermore I found a few bugs or maybe just deprecated functions, variables which I couldn’t get running on my system.

I’m using Synf 1.2 and Windows 7 with Xampp.

Bye

17 06 2009
Anthony

Amazing!

This is exactly what I was dreaming to accomplish since I started reading and fiddling about symfony 4 days ago..
Nested forms coming in and out with ajax!!

I am only at the beginning of fully understanding symfony and I am loving it!

Now, I noticed that the jobeet tutorial and latest versions uses doctrine, and most other stuff uses propel..

Any pros and con noticeable from those two?
And how much different your example would be to use doctrine instead?

Thanks

P.S. Next step, integrating kerberos where the authentication lies =)

17 06 2009
israelwebdev

I don’t have any experience working with Doctrine. You can search for “propel vs doctrine” to see a few useful docs. Based on a few comments, it seems that new users might be better off jumping on the Doctrine bandwagon.

18 06 2009
shmayek

Very nice. I need very similar solution, but user need to have a choice to add new item or choose existing one from list. The same solution for Customer. There is only combobox with customers, but there is no button for create one without cancelling form (regular issue for desktop apps). Maybe you know a good tutorial or similar solutions to look for?

23 06 2009
israelwebdev

This might be what you’re looking for:
http://www.symfony-project.org/plugins/pkAdminQuickCreatePlugin

31 07 2009
Gaston

Hi! Your post is great!

One question: I get this error:

Unknown method VentaDetalle::setDeleted

line:
$this->embeddedForms[‘item_pos’.$i]->getObject()->setDeleted(true);

Any advice? Thank you in advance!

1 08 2009
israelwebdev

Are you using the latest version of Propel packaged with Symfony 1.2? That function is in the BaseObject class, which should be inherited by your Object class.

24 08 2009
Anton

It doesn’t work when there are files in the embedded forms unfortunately..

24 08 2009
israelwebdev

It shouldn’t make a difference, but I haven’t tried it.
I’m sure there’s a way to get it to work right – if you find the solution, let us know.

2 09 2009
4levels

Hi there,

I’m just wondering how this story works with I18n enabled as the labels are running throught the __() function. I’m not sure if the I18n engine skips HTML / JavaScript tags (I sure hope it does!) because if it doesn’t ignore them, this implementation breaks the translation function as you cannot add translations for all the different jquery calls.

Has anyone experience with this?

PS. The same applies if one would use the setHelp instead of the setLabel method..

Kind regards and keep up the great work!

20 10 2009
mila_1881

hi everybody!!
I’am new with the symfony 1.2 (I used to work with the 1.0 version)

I find that this script is great and very useful that really helped me.
I changed it a bit to suit my needs but when I add a new embedded form and I submit the form I get this error:
Unexpected extra form field named “item_aps2”.
there is my code :
// lib/forms/ActiviteArtisanaleForm.class.php
public function configure()
{
....
sfLoader::loadHelpers(array('jQuery','Asset','Tag','Url'));
$index = 0;
foreach ($this->getObject()->getArtisanPrimes() as $ap){
$art_prim_form = new ArtisanPrimeForm($ap);
$art_prim_form_widget_schema = $art_prim_form->getWidgetSchema();
$art_prim_form_widget_schema['tel'] = new sfWidgetFormInputDelete(array(
'url' => 'activite_artisanale/deleteItem?idd='.$ap->getId(),
'model_id' => $ap->getId(),
'confirm' => 'Sure???',
));
$this->embedForm('item_aps'.++$index, $art_prim_form);
$label = "Item $index"
$this->widgetSchema->setLabel('item_aps'.$index,$label);
}
$arp = new ArtisanPrime();
$arp->setActiviteArtisanale($this->getObject());
$this->embedForm('item_aps'.++$index, new ArtisanPrimeForm($arp));
$this->widgetSchema->setLabel('item_aps'.$index,"Item $index");
$label = "Item $index".jq_link_to_remote(image_tag('/sf/sf_admin/images/add'), array(
'url' => 'activite_artisanale/addItems?aaid='.$this->getObject()->getPatrimoineImmaterielId().'&index='.$index,
'update' => 'sf_fieldset_none',
'position'=> 'bottom',
));
$this->widgetSchema->setLabel('item_aps'.$index,$label);
sfContext::getInstance()->getUser()->setAttribute('N1added'.$this->getObject()->getPatrimoineImmaterielId(), $index);

// apps/frontend/modules/ActiviteArtisanale/actions/actions.class.php
public function executeDeleteItem(sfWebRequest $request) {
$artisan_prime = ArtisanPrimePeer::retrieveByPk($request->getParameter('idd'));
$artisan_prime->delete();
$this->redirect('@activite_artisanale_edit?patrimoine_immateriel_id='.$artisan_prime->getActiviteArtisanale()->getPatrimoineImmaterielId());
}

public function executeAddItems(sfWebRequest $request){
$activite_artisanale_id = $request->getParameter('aaid');
$index = sfContext::getInstance()->getUser()->getAttribute('N1added'.$activite_artisanale_id);
sfLoader::loadHelpers(array('jQuery','Asset','Tag','Url'));
$form = new ActiviteArtisanaleForm();
$form->setWidgets(array('patrimoine_immateriel_id' => new sfWidgetFormInputHidden(),));
$form->setValidators(array('patrimoine_immateriel_id' => new sfValidatorPropelChoice(array('model' => 'ActiviteArtisanale', 'column' => 'patrimoine_immateriel_id', 'required' => false)),));
$widgetSchema = $form->getWidgetSchema();
$widgetSchema->setNameFormat('activite_artisanale[%s]');
$ap = new ArtisanPrime();
if($activite_artisanale_id) $ap->setActiviteArtisanaleId($activite_artisanale_id);
$form->embedForm('item_aps'.++$index, new ArtisanPrimeForm($ap));
$widgetSchema->setLabel('item_aps'.$index,"Item $index");
$label = "Item $index".jq_link_to_remote(image_tag('/sf/sf_admin/images/add'), array(
'url' => 'activite_artisanale/addItems?aaid='.$activite_artisanale_id.'&index='.$index,
'update' => 'sf_fieldset_none',
'position'=> 'bottom',
));
$widgetSchema->setLabel('item_aps'.$index,$label);
sfContext::getInstance()->getUser()->setAttribute('N1added'.$activite_artisanale_id, $index);
$this->form = $form;
return $this->renderPartial('activite_artisanale/items' );
}

there is the partial _items

getFormFields($form, 'new') as $fields): ?>
$field): ?>
isHidden()) || (!isset($form[$name]) && $field->isReal())) continue ?>
$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,
)) ?>

// lib/forms/ActiviteArtisanaleForm.class.php
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
... for($i=1;$igetUser()->getAttribute('N1added'.$this->getObject()->getPatrimoineImmaterielId());$i++)
{
if(!isset($taintedValues["item_aps$i"]) || empty($taintedValues["item_aps$i"]['tel']))
{
if($this->embeddedForms['item_aps'.$i]->getObject())
$this->embeddedForms['item_aps'.$i]->getObject()->setDeleted(true);
unset($this->embeddedForms["item_aps$i"], $taintedValues["item_aps$i"]);
$this->validatorSchema['item_aps'.$i] = new sfValidatorPass();
} else {
$this->embeddedForms['item_aps'.$i]->getObject()->setActiviteArtisanale($this->getObject());
$artisanPrime_embedded_forms = $this->embeddedForms["item_aps$i"]->getEmbeddedForms(); $artisanPrime_embedded_forms['artisanPrime_i18n']->getObject()->setArtisanPrime($this->embeddedForms["item_aps$i"]->getObject());
$artisanPrime_embedded_forms['artisanPrime_i18n']->getObject()->setCulture($cult);
}
}
}

I think that it is an error of validation but I don’t know what shall I do
please help!!

21 10 2009
israelwebdev

You need to consider 3 cases:
1-User removes a field that existed on page load
2-User adds a dynamic form/field, but doesn’t enter data there
3-User adds a dynamic form/field and enters data to be recorded.

Under which of the above conditions does your form fail?
The first 2 cases are handled by the if clause in the bind function, and the 3rd case is handled by the else clause.

In the if clause, did you try unsetting the embedded form in the validatorSchema (like the example) instead of issuing a Pass? Since we already erased the traces of this embedded form, including it in the validatorSchema should confuse the form processing enough to issue an error.

22 10 2009
mila_1881

I relpaced my if clause by yours.

if(!isset($taintedValues["item_aps$i"]) || empty($taintedValues["item_aps$i"]['id']))
{
if($this->embeddedForms['item_aps'.$i]->getObject())
$this->embeddedForms['item_aps'.$i]->getObject()->setDeleted(true);
unset($this->embeddedForms["item_aps$i"], $this->validatorSchema["item_aps$i"]);
unset($taintedValues['item_aps'.$i]);
}

….
but my form fails in cases 2 and 3 (same error)
what it does mean??

22 10 2009
jaime

Hi, it’s a very good tutorial.

I have a question, when a validation error happens does the embedded forms via ajax disappear or they stay there?

Because I have embedded forms via ajax, but in other way than the made by you, and they disappear when happens an validation error.

If with your method the don’t disappear I’ll try it.

Thanks for all your work.

22 10 2009
israelwebdev

Thank you Jaime. I haven’t tried my embedded forms with Validation, but it should work as expected. Maybe someone else can respond, or you can try it and let us know.

22 10 2009
israelwebdev

Is “id” a required field in your embedded form, from the point of view of being completed?
I used this code to test if an embedded form should be removed and not recorded:
|| empty($taintedValues["item_pos$i"]['item_id'])

If the item_id field is not entered, the embedded form is removed.
Have you entered echo statements in your code, to see if the proper blocks are being entered?

Also, make sure that, if you’re using the form name format activite_artisanale[%s], that all fields of the main form, on page load embedded forms, and the AJAX embedded forms, all match that pattern.

23 10 2009
mila_1881

thxx israelWebDev for efforts !!
me too I use “tel”( and not “id” : the code contains a little error sorry)
to test if an embedded form should be removed and not recorded.
I used firebug to inspect all fields (initial embedded forms and the AJAX embedded forms) they take same name format: activite_artisanale[%s].

so how can I validate the ajax embedded form (at which level and how do this) ???

13 11 2009
Pedro Casado

@Anton: Did you get it to work with files?

25 11 2009
Bat

Hi,

Thanks for thisvery good tutorial.

But I got a problem with the addItems() method which returns nothing.
When I call directly the action by its URL (not with AJAX), I got a blank page.

An idea about this problem ?

Thanks

25 11 2009
israelwebdev

This might be an example of a greater problem where Symfony returns a blank white page on some errors.
I don’t recall seeing this problem here – try debugging by commenting some code and see if you can pinpoint the problem.

25 11 2009
Bat

Thanks for your response, but it seems there is no error : I’ve call the page by its URL, with the “dev” front controler.

An echo works great….

But it seems that the $configuration->getFormFields returns wrong fields in the partial….

3 12 2009
Erin

I’ve written a tutorial on implementing a similar dynamic form system but using Doctrine and not involving the admin generator. The implementation is quite flexible and could be applied to many situations. If you have time, please check it out at http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ I’d be very interested to hear your thoughts.

3 12 2009
israelwebdev

Erin,
You clearly put more effort into your tutorial than I did with mine. A+
I sort of piggy-backed my code on the admin generator, auto-creating the skeleton for me, as I’ve done with Symfony 1.0 prior to the Forms objects. With the sfForm class however, you are correct that it does the work itself, and you don’t need the admin generator much.
Great contribution.

7 08 2010
Ezzatron » Expanding forms with symfony 1.2 and Doctrine

[…] you are interested, there is a guide to implementing a similar system here, however it seems to deal with the symfony admin generator specifically, and it also seems to have […]

18 11 2010
Storage Chest

those generic mp3 players that are made in china are really cheap but i still prefer to use my ipod .~:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s




%d bloggers like this: