A special form of the Loop component that adds Ajax support to handle adding new rows and removing existing rows dynamically. Expects that the values being iterated over are entities that can be identified via a ValueEncoder. Works with AddRowLink and RemoveRowLink components. The addRow event will receive the context specified by the context parameter. The removeRow event will receive the client-side value for the row being iterated.
| Name | Type | Flags | Default | Default Prefix | Since | Description |
|---|---|---|---|---|---|---|
| addRow | Block | NOT Allow Null | block:defaultAddRow | literal | A block to render after the loop as the body of the FormInjector. This typically contains a AddRowLink. | |
| context | Object | NOT Allow Null | prop | The context for the form loop (optional parameter). This list of values will be converted into strings and included in the URI. The strings will be coerced back to whatever their values are and made available to event handler methods. | ||
| element | String | NOT Allow Null | literal | The element to render for each iteration of the loop. The default comes from the template, or "div" if the template did not specify an element. | ||
| encoder | ValueEncoder | Required, NOT Allow Null | prop | Required parameter used to convert server-side objects (provided from the source) into client-side ids and back. A default encoder may be calculated from the type of property bound to the value parameter. | ||
| show | String | NOT Allow Null | literal | Name of a function on the client-side Tapestry.ElementEffect object that is invoked to make added content visible. This is used with the FormInjector component, when adding a new row to the loop. Leaving as null uses the default function, "highlight". | ||
| source | Iterable | Required, NOT Allow Null | prop | The objects to iterate over (passed to the internal Loop component). | ||
| value | Object | Required, NOT Allow Null | prop | The current value from the source. |
package org.example.addressbook.entities;
import org.apache.tapestry5.beaneditor.NonVisual;
import org.apache.tapestry5.beaneditor.Validate;
import org.apache.tapestry5.beaneditor.Width;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import javax.persistence.*;
import java.util.List;
@Entity
public class Person
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NonVisual
private long id;
. . .
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<Phone>();
. . .
public List<Phone> getPhones()
{
return phones;
}
public void setPhones(List<Phone> phones)
{
this.phones = phones;
}
}
package org.example.addressbook.entities;
public enum PhoneType
{
HOME, OFFICE, MOBILE, FAX, PAGER
}
package org.example.addressbook.entities;
import org.apache.tapestry5.beaneditor.NonVisual;
import org.apache.tapestry5.beaneditor.Validate;
import org.apache.tapestry5.beaneditor.Width;
import javax.persistence.*;
@Entity
public class Phone
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NonVisual
private long id;
@ManyToOne(optional = false)
private Person person;
private PhoneType type;
@Column(nullable = true, length = 20)
@Width(20)
@Validate("required,maxlength=20")
private String number;
public long getId()
{
return id;
}
public void setId(long id)
{
this.id = id;
}
public Person getPerson()
{
return person;
}
public void setPerson(Person person)
{
this.person = person;
}
public PhoneType getType()
{
return type;
}
public void setType(PhoneType type)
{
this.type = type;
}
public String getNumber()
{
return number;
}
public void setNumber(String number)
{
this.number = number;
}
}
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<body>
<h1>Edit ${person.firstName} ${person.lastName}</h1>
<t:form t:id="form">
<t:errors/>
<div class="t-beaneditor">
<t:beaneditor t:id="person"/>
<h2>Phones</h2>
<div t:type="ajaxformloop" t:id="phones" source="person.phones" encoder="phoneEncoder" value="phone">
<t:select t:id="type" value="phone.type"/>
<t:textfield t:id="number" value="phone.number"/>
|
<t:removerowlink>remove</t:removerowlink>
</div>
<p>
<input type="submit" value="Update"/>
</p>
</div>
</t:form>
</body>
</html>The AjaxFormLoop provides a default row for adding additional data rows.
package org.example.addressbook.pages;
import org.apache.tapestry5.PrimaryKeyEncoder;
import org.apache.tapestry5.annotations.PageActivationContext;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.hibernate.annotations.CommitAfter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.example.addressbook.entities.Person;
import org.example.addressbook.entities.Phone;
import org.hibernate.Session;
import java.util.List;
public class Edit
{
@PageActivationContext
@Property
private Person person;
@Property
private Phone phone;
@Inject
private Session session;
public PrimaryKeyEncoder<Long, Phone> getPhoneEncoder()
{
return new PrimaryKeyEncoder<Long, Phone>()
{
public Long toKey(Phone value)
{
return value.getId();
}
public void prepareForKeys(List<Long> keys)
{
}
public Phone toValue(Long key)
{
return (Phone) session.get(Phone.class, key);
}
};
}
@CommitAfter
public Object onSuccess()
{
return Index.class;
}
@CommitAfter
Object onAddRowFromPhones()
{
Phone phone = new Phone();
person.getPhones().add(phone);
phone.setPerson(person);
return phone;
}
@CommitAfter
void onRemoveRowFromPhones(Phone phone)
{
session.delete(phone);
}
}
The onAddRowFromPhones() event handler method's job is to add a new Phone instance and connect it to the Person. The @CommitAfter annotation ensures that changes are saved to the database (including generating a primary key for the new Phone instance).
The flip side is onRemoveRowFromPhones(), which is the event handler when removing a row. The event handler method is passed the Phone object to remove. Again, it is necessary to commit the Hibernate transaction.
The minimal implementation of a PrimaryKeyEncoder is also shown; this one is customized for Phone instances, and knows how to extract primary keys (the id property) and convert primary keys back into objects. This could easily be rolled out as a Tapestry IoC service.