J2EE developer with over 7 years of experience in designing and implementing enterprise j2ee solutions based on open source technologies like Tapestry, Hibernate, Spring. Current interests include Tapestry, Plastic, Spock, Scala. Taha is a DZone MVB and is not an employee of DZone and has posted 40 posts at DZone. You can read more from them at their website. View Full User Profile

Tapestry & AjaxFormLoop

07.31.2011
| 3422 views |
  • submit to reddit

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>

From http://tawus.wordpress.com/2011/07/26/tapestry-ajaxformloop/

Published at DZone with permission of Taha Siddiqi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)