02.05.09

Stretching sfForm with Dynamic Elements(AJAX): A Love Story

Posted by ryan in php, symfony


sfForm meet AJAX, AJAX meet sfForm

kiss.jpg
Let's introduce our players. First, meet sfForm, Symfony's base class for its powerful (relatively new) form framework. Known best for kicking your ass, sfForm is revered by many (like me) and feared by most (like me). He's a looming, dark presence, like having a 6 foot security guard standing in the corner at your daughter's 2nd birthday party. He guards you while you sleep. Do not... fuck with him.

Now, meet AJAX, the javascript method that brings hope to average web applications everywhere. She's the reason that your girlfriend (and you) loves facebook and she's always ready to make your web experience feel more comfortable. When AJAX is around, everything just feels faster, easier...better. She's the Obama of the internet: Yes we can.

Of course, in order to have a well-written server-side application as well as a rich user experience, these two components must be joined together. In reality, it's really not too hard, it just takes a little bit of practice.

The Goal

Simple: first, take a look at my previous article about building a simple todo list. Below is our target application (click to actually use it). The big addition now is that we've added a link you can click to add more input boxes. This is done through AJAX and our form is able to handle the "extra" elements.

Our advanced todo list
todo-forms-advanced.gif

The Very Simple Setup (same as the simple todo list)

We're using the exact same setup as with the simple todo list. Some things have different names, but to start, the code is all the same. Click the link below to view the code.

Show/Hide code

Adding the Magic

So far, all we have is the same thing as our simple todo form. To add additional input boxes, we're going to make an AJAX request. I use Jquery and post that code here. The most important thing, however, is that you make an ajax call (whatever your preferred method) to the action todo/advancedAddTodoAjax and spit out the result somewhere inside your form. As you can see below, I keep a count of my new todos and send it along to our action under the parameter name 'todo_index'.

<form...>
  ...
  <a href="#" onclick="return add_todo();" class="add-another" title="add another todo item box">Add another</a>
</form>
<script type="text/javascript">
  var new_todo_index=2;
  
  add_todo = function()
  {
    $.post(
      '<?php echo url_for('todo/advancedAddTodoAjax'); ?>',
      { todo_index: new_todo_index },
      function(data, textStatus) {
        $('#todo-wrapper').append(data);
      }
    );
    new_todo_index = new_todo_index+1;
    
    return false;
  }
</script>

The AJAX Action

The job of the AJAX action is simple: to return a new todo widget that will function nicely with our form when it's finally submitted. Doing this is no different than any other situation where we output form elements. Like always, we setup our form, then render its elements. The only difference is that in this case, we only need to render 1 element (our new todo widget) instead of the entire form.

  public function executeAdvancedAddTodoAjax(sfWebRequest $request)
  {
    $todoIndex = $request->getParameter('todo_index');
    $this->forward404Unless($todoIndex);
    
    $embedName = 'new_'.$todoIndex; // the unique name to give this form
    $form = new advancedTodoForm();
    $form->addNewTodoField($embedName);
    
    $output = $form['todos'][$embedName]['task']->render();
    $this->renderText($output);
    
    return sfView::NONE;
  }

Notice that we used the todo_index request parameter to form a unique name for our new embedded form. Since the first argument in embedForm is used to construct the name attribute of each element, it's important that each be unique.

The action is simple: build your form like normal, then add in a new field. This ensures that our new input element is output as if it's been a part of the original form all along. The action introduces a new method in our form, seen here:

class advancedTodoForm extends sfForm
{   
  ...  
  public function addNewTodoField($name)
  {
    $this->embeddedForms['todos']->embedForm($name, new TodoForm());
    $this->embedForm('todos', $this->embeddedForms['todos']); // re-embed the form
  }
}

This method embeds a new blank TodoForm into the already-embedded 'todos' form. The second line "re-embed"s the 'todos' form into the main form. This line may/should look redundant to you. It's required due to the way that sfForm embeds forms. The first line alone doesn't fully embed our new blank TodoForm.

Progress Check - AJAX is up and running

At this point, you should add the following to your view right after $form->renderGlobalErrors(). Whether it be a bug or a feature, you'll need it or you won't see certain validation errors. In my opinion, if this isn't a bug, I'd like to hear the explanation as this seems like very sloppy behavior.

<?php echo $form['todos']->renderError(); ?>

You should now be able to click your "Add another" link as many times as you want to add more and more blank todo boxes. If you click save with any extra boxes, you'll get an "extra fields" error. Here's what's happening:

  1. You click "add another" and a request is made to the server, which inserts a new blank todo box into your HTML
  2. You click "save" and are sent to the advancedProcess action
  3. This action sets up your default advancedTodoForm, which DOES NOT include the new todo element that was present in your HTML as a result of the AJAX call
  4. The Symfony form validates your data and sees the additional AJAX element as "extra" data

Getting romantic with sfForm and AJAX, aka binding to the dynamic elements

As described above, the advancedTodoForm isn't aware of your new todo input fields and thus rejects them. This is where the rigidity of sfForms (good for security) meets the dynamic requirements of AJAX. Fortunately, the solution was quite simple.

class advancedTodoForm extends sfForm
{
  ...
    
  /**
   * Override to add extra form fields for new todo items (otherwise they fail as extra fields)
   */
  public function bind(array $taintedValues = null, array $taintedFiles = null)
  {
    foreach($taintedValues['todos'] as $key=>$newTodo)
    {
      if (!isset($this['todos'][$key]))
      {
        $this->addNewTodoField($key);
      }
    }
    
    parent::bind($taintedValues, $taintedFiles);
  }
  ...
}

In order to avoid validation errors, we need to properly add the missing form elements before our form is bound. To do this, we override the bind() method with the above code. The idea is stupidly simple: If our input data contains a todo field that doesn't correspond to an embedded TodoForm form, add it. Notice we use the addNewTodoField method - the same one used in our AJAX action. We're constructing our form (adding todo forms to it) to reflect the new fields that were added via AJAX.

And that's it. Now that our dynamic todo fields are detected and their forms are embedded, each form is automatically and correctly saved via our existing save function.

Is It Love?

All that I know for sure is that I love both the Symfony forms framework and sites that use AJAX to make the web function smoothly. With the forms framework being young and the importance of AJAX only being compounded, I expect that the relationship between the forms framework and AJAX will only improve. Personally, I'm actually very happy with the above solution - but I'm pretty easy to please. If not love, it's certainly the start to a beautiful relationship: it has to be.

And Happy Valentine's day in a week or so. Happy Valentine's day!

Attachments

Simple and Advanced Todo Module (todo.tar.gz)
Thanks for the shares!
  • StumbleUpon
  • Sphinn
  • del.icio.us
  • Facebook
  • TwitThis
  • Google
  • Reddit
  • Digg
  • MisterWong
Posted by Paul on 2009-02-08
Hi! your post is wonderfull!
I've follow all the steps, but in my pc I've some errors.. Can you publish an archive with all the project, please?
Thanks in advance
Posted by Ryan on 2009-02-08
Thanks Paul - I've attached an archive of the todo module here. This should cover both the simple and advanced todo lists (as well as a test case for what I think is a bug).

Let me know if anybody has any problems with the above.
Posted by Paul on 2009-02-09
Thanks Ryan. whit your files all work perfectly!
Now, however, I want to apply your guide to my case, but I don't obtain what I would..
I've create a topic in the symfony forum; your help is appreciated!! :)
You can find the topic here: http://forum.symfony-project.org/index.php/m/71886/#msg_71886

Thanks for your guide!
Posted by javier on 2009-03-10
I need to know where are from in this code the var: todo_index: new_todo_index
***********************
add_todo = function()
{
$.post(
'',
{ todo_index: new_todo_index },
function(data, textStatus) {
$('#todo-wrapper').append(data);
}
);
new_todo_index = new_todo_index+1;

return false;
}

thanks for any help and for the guide
Posted by Ryan on 2009-03-10
@javier

The variable new_todo_index is simply a javascript variable initialized just before the javascript function add_todo(). It's purpose is to ensure that each new todo field has a unique name (e.g. todo_list[todos][new_1], todo_list[todos][new_2], etc). The advancedAddTodoAjax action looks for this value to be passed with the request via a parameter called 'todo_index'.

I hope that clarifies!

-Ryan
Posted by Futuro on 2009-04-04
This sample usage of embedded forms and ajax is very useful, but I would be nice If you show for example how to dynamically delete anyone of the embedded forms. I mean for example by clicking delete link near by chosen field of embeded form. Is it possible with ajax ?
Posted by David Maignan on 2009-04-16
A huge thanks for your posts on embedform. Very well explained overall.
Posted by gonetil on 2009-04-26
Great article Ryan! However, I have tried to use it in my project and I keep getting an error in JS code, saying:
$.post is not a function .
What am I doing wrong? have I forgotten to include any helper? or I am just missing something?

Regards
Posted by Ryan on 2009-04-29
@gonetil

Be sure to include the Jquery library in your header - I may not have made this as clear as I should have:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
Posted by michel on 2009-05-21
Hi,
Great tuto ;)
I am beginner in symfony and I would like to kno if there is anyway to limit the number of adding fields to 5.
Thanks
Posted by Ryan on 2009-05-22
@michel

Absolutely. To do this, you'd have to do a few things:

1) your action method executeAdvancedAddTodoAjax needs to be smart enough NOT to return a new field if there are already 5 fields. Specifically, something like this in there:
...
$todoIndex = $request->getParameter('todo_index');
$this->forward404Unless($todoIndex);
$save_todo_count = TodoPeer::doCount(new Criteria());
if (($save_todo_count + $todoIndex) > 5)
{
$this->renderText('You cannot add more than 5 todos');
return sfView::NONE;
}
...

2) You'll need to add code into your advancedTodoForm::configure method to NOT automatically embed a blank todo form if there are already 5 todo objects.
3) If you already have 5 todos, you should hide the "Add Another" link in your template
4) You should add a custom post validator to your advancedTodoForm that tests to make sure that there wasn't more than 5 todos forms embedded.

So, it's not simple, not hard. As it wasn't originally designed for that, there's just several things that need tweaked for it to work out.

Let me know if that helps.
Posted by michel on 2009-05-22
Thanks you Ryan.

I will try it. I don't know how to do point 2 and 4 but i will seearch.
Again, thanks you.
Best regards
Mike
Posted by Rémy on 2009-07-06
Hi,
First, I(d like to say : Nice tutorial.
I tried your tricks on ma case.
I used it upload multiple files. Pictures are saved but I always have this error, in the bind function:

Warning: Invalid argument supplied for foreach() in ...
Posted by Raphaël on 2009-11-04
Hi, nice tutorial.
This works for a rather simple embedded forms.
But how do you handle a little more complicated one:

I have Owner who owns several buildings which have several tenant.

how to handle the binding of the tenant subforms, since there is no binding?

And I want to avoid to put some "tenant" logic into the "owner form".?
Posted by Erin on 2009-12-03
If you're looking for a slightly more complex / in-depth tutorial check out http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ - it details how to have subforms with multiple fields amongst other improvements.
Posted by shirkavand on 2010-05-29
hi,

Why you are embeding a form if the only thing you want to do is adding an input widget?...maybe i am wrong but the idea of embeding forms is, for example, if the user want to upoad multiple pictures...so i embed as many upload forms as the user requests...but in this "todo" case every "todo" you add is not a form...am i right?
Posted by Ryan on 2010-05-29
Hey shirkavand-

Yes, you are correct. More generally, you'd want to embed a form anytime you want to create multiple database objects in one form.

In this case, the Todo items are in fact a separate object, which were kept purposefully simple just for illustration. If you truly has a model with only one field, there may be an easy way to do this, or perhaps that model shouldn't be a model.

So, you're understanding is correct - this is just a oversimplified example.
Posted by Caron on 2010-12-18
Might have to taint some of your love ... This method is realy nice, until you come to the Point wer you want ot embed this form into another Form, espacially an sfFormObject. In that cafe, the saveEmbeddedForms method of sfFormObject will dive into the TodoForm obejects, overriding your save() method. Best, but not good solution in found until now is to make the advanced* form a sfFormObject (with is strictly not correct) and make your save() method a saveEmbeddedForms method ...

If you are interested, feel free to contct me for a discussion.
Posted by Caron on 2010-12-18
No, let me take that back (together with all the typing errors ...). Making it an sfFormObject ist also shitty ...
Can't see any solution but overriding the saveEmbeddedForms method in the embedding form.
Posted by Ryan on 2010-12-20
@Caron

Yes, you've hit on a negative aspect of embedding forms. There are several things you can override in the parent form to perform things on the child, embedded forms. All of these things fail, however, as soon as the parent form itself embedded. This is just a known drawback. The best thing you can do would be to break out the special functionality into its own method. Then, in the parent form, you could call that method from doSave() (for example). If your parent form is itself embedded, you could still call this method from doSave() of the super-parent form. It's far from perfect for sure.
Posted by Pepe on 2011-04-01
Maybe this question is stupid, but i'll ask it anyway.
Who and where declares class TodoForm ??? You use it, but I can not find the declaration of it...
Posted by Pepe on 2011-04-01
Sorry... missed the last part of the 'The Very Simple Setup' :-(
Posted by anil gupta on 2011-06-22
can u give any example in doctrine
plz
Posted by Throoze on 2011-12-15
Hi! this is really a great tutorial! there are just two things i didn't get:

If there are blank todo forms, you discard them by overriding the save() method, but, What happens in the isValid() method? Why doesn't it validate blank embedded forms?

The other thing is what Caron comments. That is my case, and I don't understand your proposed solutions. Could you clarify them more please? Here's my mail just in case: rdbvictor19@gmail.com