Tapestry & AjaxFormLoop

Tapestry mailing list has a constant flow of newbie questions related to AjaxFormLoop component. This is a very powerful component but with some limitations that must be understood before using it.

AjaxFormLoop allows, in a limited way, dynamic addition of components to a form. These components are laid out inside the AjaxFormLoop. Each time the ‘Add New’ link is clicked, addRow event is triggered. This event requires the event handler to return a new ‘value’ bean. A new row is added to the loop with the given set of components and these components if form fields are bound to the newly instantiated bean.

During rendering, a hidden field is inserted into each row and its value is set to the string coercion of loop value parameter. On submit this value is coerced back to the value. The whole coercion part is handled by the ValueEncoder parameter (“encoder”)

A simple example is show below

public class SimpleLoop
{
   @Property
   @Persist
   private List<Foo> foos;

   @SuppressWarnings("unused")
   @Property
   private Foo foo;

   void onActivate()
   {
      if(foos == null)
      {
         foos = new ArrayList<Foo>();   
      }
      
   }
   
   void setupRender(){
      foos.removeAll(Collections.singleton(null));
   }
   public ValueEncoder<Foo> getEncoder(){
      return new ValueEncoder<Foo>()
      {

         public String toClient(Foo foo)
         {
            return String.valueOf(foos.indexOf(foo));
         }

         public Foo toValue(String clientValue)
         {
            return foos.get(Integer.parseInt(clientValue));
         }
         
      };
   }

   Object onAddRow()
   {
      Foo newFoo = new Foo();
      foos.add(newFoo);
      return newFoo;
   }
   
   void onRemoveRow(Foo newFoo)
   {
      foos.set(foos.indexOf(newFoo), null);
   }
   
   void onSuccess()
   {
      foos.removeAll(Collections.singleton(null));
   }

}

<html xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'>

   <body>

      <t:if test='foos'>${foos}</t:if>
      
      <form t:type='form'>
         <div t:type='ajaxformloop' t:source='foos' value='foo' encoder='encoder'>
            <label t:type='label' t:for='bar'></label>: 
            <input t:type='textfield' t:id='bar' t:value='foo.bar'/> 
            <t:removerowlink>remove</t:removerowlink>
            <br />

         </div>
         <input type='submit' t:type='submit' value='Submit' />
      </form>
   
   </body>
   
</html>

The value encoder here uses the index as key. Usually a ValueEncoder is based on an entity’s primary key.

Now the real confusion about AjaxFormLoop. What if there is an ajax component within a row. If an ajax component makes an ajax request, that request is going to find the ‘value’ bean to be null as it is not persisted. What if we persist it ? If we persist, that will result in another problem. During an ajax request, the loop is not iterated again and so the persisted value is the last value that was read during rendering. All the components are temporarily bound to this last value. Therefore, persisting is a bad idea. The only way you can get around this problem is to not trust the ‘value’ field and instead use context in the ajax call. The context can then be used to get the actual value (e.g. fetching it from the database, in which case the context will be the primary key).

Here is an example where each row contains an eventlink with context set to a unique key. This key is then used for uniquely identify foo.


/**
 * AjaxFormLoop with ajax updates.
 */
public class LoopWithAjaxUpdates
{
   @Persist
   @Property
   private List<Foo> foos;

   @Property
   private Foo foo;

   @InjectComponent
   private Zone zone;
   
   void onActivate()
   {
      if(foos == null)
      {
         foos = new ArrayList<Foo>();
      }

   }
   
   public String getUniqueZoneId()
   {
      return "zone_" + foos.indexOf(foo);
   }
   
   public int getId()
   {
      return foos.indexOf(foo);
   }

   public ValueEncoder<Foo> getEncoder()
   {
      return new ValueEncoder<Foo>()
      {

         public String toClient(Foo foo)
         {
            return String.valueOf(foos.indexOf(foo));
         }

         public Foo toValue(String clientValue)
         {
            return foos.get(Integer.parseInt(clientValue));
         }

      };
   }

   Object onAddRow()
   {
      Foo newFoo = new Foo();
      foos.add(newFoo);
      return newFoo;
   }

   void onRemoveRow(Foo newFoo)
   {
      foos.set(foos.indexOf(newFoo), null);
   }

   void onSuccess()
   {
      foos.removeAll(Collections.singleton(null));
   }

   Object onZoneUpdate(int index)
   {
      foo = foos.get(index);
      return zone.getBody();
   }
}
<html xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'>

   <body>

      <t:if test='foos'>${foos}</t:if>

      <form t:type='form'>
      
         <div t:type='ajaxformloop' t:source='foos' value='foo' encoder='encoder'>

            <label t:type='label' t:for='bar'></label> :
            <input t:type='textfield' t:id='bar' t:value='foo.bar' />
            <a href='#' t:type='eventlink' t:event='zoneupdate' 
                t:context='id' t:zone='${uniqueZoneId}' >update</a> | 
            <t:removerowlink>remove</t:removerowlink> <
            <span t:type='zone' t:id='zone' id='${uniqueZoneId}'>
                 ${foo.bar}</span> > 
            <br />

         </div>
         
         <input type='submit' t:type='submit' value='Submit' />
      </form>

   </body>

</html>
About these ads

Tagged: , , ,

One thought on “Tapestry & AjaxFormLoop

  1. kot9rko September 2, 2011 at 12:40 PM Reply

    Hi.
    It seems there’s a problem in the first example (at least in FF 4). If I try to click on “remove” link and then quickly hit “submit” button (while animation occurs), I got an exception: “Unable to convert client value … back into a server-side object.”
    I’ve fixed it by adding a List, where I store items to delete, instead of making them null, so they can be removed in onPrepareForRender form handler.

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

Follow

Get every new post delivered to your Inbox.

Join 90 other followers

%d bloggers like this: