Easy ajax forms with sfDoctrineDynamicFormRelationsPlugin

Here’s the case: you want (badly!) to add a form to an existing one through ajax. You know, like it wasn’t there the minute before but was only one click away! You want to make that click and have all the new form fields appear right here right now for your own pleasure! This new form of course is one of a related object. We’ll say for example that you have a table of customers and one to store addresses. Of course it happens that sometimes a customer might need to fill more than one address. Your form will ship with one, two or zero address forms, but a small link will say ‘add an address’ and the correct fields will appear. Then this link will stay and you’ll be able to add many more addresses!

My case was with companies to which I want to add some brands. In this case, the result would look like this:

Getting and installing the plugin

First go to http://github.com/kriswallsmith/sfDoctrineDynamicFormRelationsPlugin and download the plugin. Unpack it, and rename the folder in which it is contained to only ‘sfDoctrineDynamicFormRelationsPlugin’.

Add

$this->enablePlugins('sfDoctrineDynamicFormRelationsPlugin');

To the setup method of your ProjectConfiguration.class.php file. Now from the command line run:

symfony plugin:publish-assets

The most difficult is almost done!

Using the plugin

The plugin stays out of the way most of the time, but since it requires some ajax calls and some javascript stuff, it means you still have some work to do! For all the javascript needed, I’ll just show how I did it through jQuery, but anything else could be used. The case I was working on is that I have a company model to which I might have to add some brands. The model could be like this:

The model

Company:
 columns:
   name:                           string(255)
   short_name:                     string(255)
   status_id:                      integer
   phone:                          string(255)
   email:                          string(255)

Brand:
 columns:
   name:                           string(255)
   short_name:                     string(255)
   company_id:                     integer
 relations:
   Company:
     foreignAlias:                 Brands

The main form class

In your CompanyForm class, just as the plugin’s readme suggests, you will add to the configure() method the following line:

$this->embedDynamicRelation('Brands');

That’s it for this class!

Showing the full form

Now let’s say you already had a company with some brands saved in your database. You can immediately show the form containing all those brands. This is done directly where you output the html for the company form. Which in my case is in a _form.php partial from the company module. In that form I just add the following lines somewhere between the form tags of the main form.

<ul id="brand_form_container">
 <?php foreach ($form['brands'] as $bookFields): ?>
  <li>
    <table><?php echo $bookFields ?></table>
    <a href="#">delete this brand </a >
  </li>
  <?php endforeach ?>
</ul>
<a href="#">Add a brand</a>

The javascript

As you can see, I have 2 links: delete this brand and add a brand that do not link to anything. Those are used through javascript to manipulate your form. The delete link is the easiest one as it does not require any ajax. When it’s clicked, we will simply remove the containing ‘li’ element, which will remove one brand form altogether. Therefore when we save the form, the brand in that form will be deleted in the database since the form wasn’t there anymore. The javascript to do this through jQuery is quite simple. Here is the whole javascript I used, including the delete function.

$(document).ready(getBrandForm);

var url = 'http://myserver.com/brand/new';
// Retrieve the form when the 'New' link is clicked
function getBrandForm(){
  $('a.get_ajax_form').click(function(){
     // load the form for Brands. Call back to attach js to the form
     $.get(url, function(data){
         $('#brand_form_container').append(data);
     })
     // return false to prevent the link's action
     return false;
   });
   // delete a form
   $('a.cleaner').click(function(event){
     $(this).parent().remove();
     return false;
   });
}

So when a link of the class ‘cleaner’ is clicked I remove the parent element. The other part is to get a new blank form to the page and add it to the list of existing forms. We simply load a form located at the /brand/new url. If you have generated your symfony module, you can almost directly use the generated form.

The related form

When you make an ajax call, symfony disables the layout by default, so you will only get the result of your newSuccess.php template. Which usually includes a _form.php partial.

In that partial, we need to do only 2 things: get rid off the <form></form> tags, since our form will be embedded into another one. Format it into a <li></li> so it appears correctly when we add it to the list. I’ll let you do that by yourselves! That’s it, you can try! And you’ll be disappointed that it doesn’t work… To make it work correctly you need to work a bit on the related form class.

In the brand form class, we’ll do this:

public function configure()
{
  $this->useFields(array(
      'name',
      'short_name',
  ));
  $this->widgetSchema->setNameFormat('company[brands][$i][%s]');
  $this->disableLocalCSRFProtection();

  .....
}
  1. The ‘company_id’ must absolutely be removed from the list of fields, otherwise the form save action will receive an empty company id and save a company not linked to anything.
  2. The widgets names must be correctly formatted to fit into the company form. This can also be done from within the action if you’re embedding let’s say addresses that could be related to a company, a person or a lot of different things. The $i is a number. It needs to be different from the ones already used in the form, but doesn’t need to follow them, you can jump from 3 to 67 if you will! Could be passed as a parameter of the /brand/new action from the javascript.
  3. Disable the CSRF protection on this form. Embedded forms in symfony are not supposed to have their own CSRF tokens. This again could be done from the action if you are using this form in other places and need to have your CSRF protection there.

If you’ve done all of that, go get some rest, it now fully works!

17 thoughts on “Easy ajax forms with sfDoctrineDynamicFormRelationsPlugin

  1. It looks great, it’s really a cool stuff. It’s a pity that it was not available before…

    A question, for wich kind of relation it works. Does it works only with 1-N, or also with 1-1 and N-N?

    Thanks!

  2. In my opinion there is no real point in using it for 1-1 relations because you could as well use the standard ‘embedRelation()’ and just hide the related form with javascript, that would also avoid a call to the server. But that should work for 1-1 provided you’re careful not to add 3 forms!
    For N-N it would “work”. My meaning is that this is made to add a related record. For example here if my brand could also belong to many companies (let’s say that Smart is a brand of Mercedes and Swatch together), then I could input Mercedes and Smart together this way, but the link from Smart to Swatch is to be provided in another place and another way.
    So this immediately and perfectly suits 1-N better than anything else!
    Hope that explains (but not quite sure!)

  3. Pingback: Tweets that mention Easy ajax forms with sfDoctrineDynamicFormRelationsPlugin | Synofony -- Topsy.com

  4. Hello,

    Excelent tutorial, actually the first one :), but i have an issue :(. I have 2 entitties News and News Images, one News can have many images
    I have followed your tutorial, created the javascript but i get a notice:
    Notice: Undefined index: id in plugins/sfDoctrineDynamicFormRelationsPlugin/lib/sfDoctrineDynamicFormRelations.class.php on line 167. Did you encounter this Notice?

    • I know that in the readme of the plugin, the author says it is mandatory to use ‘id’ as a primary key of the related model. So this maybe is your issue. I didn’t have any problem like the one you are experiencing!

        • Everything works ok, found my flaw. i need a little bit more custom graphics for the embeded forms and i forgot to output the id for the embeded forms. Thanks, and again very nice article.

    • I always try to avoid as much as possible having any javascript written in the HTML. So directly from the JS that I wrote I register some events so that my functions are called on these events like:
      $('a.get_ajax_form').click(function(){//do some stuff});
      Is equivalent to writing your HTML link with:
      onclick="//do some stuff"
      But if you do like that, you have some JS in your HTML and some JS in your JS files, it’s usually better to separate those two and to keep HTML without any JS and your JS in a separate place.

  5. Thank you for this helpful tutorial!

    One question – you say:
    “The $i is a number. It needs to be different from the ones already used in the form, but doesn’t need to follow them, you can jump from 3 to 67 if you will! Could be passed as a parameter of the /brand/new action from the javascript.”

    I don´t get it … how can I set $i from an action parameter?

    • I was wondering about the same thing.
      When I embedded a form from a doctrine relation, it would only save the last one I added. I figured out it was the $i. You actually have to give it different values each time you add a form.
      I’m still figuring out how to do that. Help would be appreciated greatly :)
      The tutorial helped me a great deal already! :D

      • You can pass it as a parameter from the javascript for example by changing the url to http://myserver.com/brand/new/i

        I absolutely can’t remember how I did it last time and since we ended up not using this, because now we save the first object once (the company) and then from the “view company” page add the brands form one at a time.

        The $i indeed as Daniel said has to be different for each form you add.

        Since there is no need for the form numbers to be in any particular order, one could also use the php function uniqid(), though it seems a bit like overkill in this case!

  6. Ok, let me explain more on the embeded form’s $i, (note to self: next time a variable is going to have this much importance in a post, DON’T name it ‘$i’)!!

    This is a number that will reference the form in your HTML. It is not linked to any database id or anything else.

    If your first brand form arrives to your page with a ‘$i’ set to 456735, that’s perfectly fine. Now when you get a second form, you will need to have a ‘$i’ that is different for the new one because it is part of the form element’s name and if your 2 elements have the same names, then when you receive your client’s data on the server side, you won’t be able to differentiate your 2 form elements.

    2 possible ways to do it:
    1- on the server side, when creating the form and setting its name:
    $this->widgetSchema->setNameFormat(‘company[brands][$i][%s]‘);
    replace $i with a call to uniqid(), this will send back a unique number every time you call it, it’s gonna be a lengthy number, but at least all your forms will have different numbers and you’re safe.
    $this->widgetSchema->setNameFormat(‘company[brands]['.uniqid().'][%s]‘);

    2- on the client side, instead of putting a $i when setting the form’s name, you’re going to set a kind of tag name to be replaced later. Example:
    $this->widgetSchema->setNameFormat(‘company[brands][%my_form_tag%][%s]‘);
    Then when you receive the form you can change it. For example in your javascript you would declare a lastFormId variable and give its value to the different forms you receive:

    Hope this helps!

  7. Hi thanks for the tutorial, it helped me alot.
    I’m using it to add/edit books that can have many authors. Unfortunately I’ve come across a problem you could perhaps help me solve. When I try to a an author that already exists the validator tells me * An object with the same “name” already exist.
    Do you know a workaround?

    thank you
    hoodie

  8. Nice guide, I followed it and got it to work with jQuery UI accordion too!

    However, I am wondering if it is possible to format the brand form by editing
    apps/backend/module/brand/config/generator.yml

    e.g. set the short name input field to size 60

    short_name:
    label: 'Brand Short Name'
    attributes:
    size: 60

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>