blob: 90d5de2c83788185afb407b4e7fc53309919aeb6 [file] [log] [blame]
<document>
<body>
<section name="Examples">
<p>
AjaxFormLoop renders an extensible, editable list of entities. It is intended for use with Master/Detail
relationships
(such as between an Order and a LineItem, in an e-commerce application). It allows new detail objects
to be added on the server side, with corresponding new user interface added to the client side.
Likewise,
existing server-side objects can be removed, and the corresponding user interface also removed.
</p>
<p>
AjaxFormLoop is dependent on the ability to extract an identifier (a primary key) from objects when
rendering, and
then retrieve the full object in a later request, such as when the form is submitted. This aligns well
with
an Object Relational Mapping layer such as Hibernate.
</p>
<p>
This example has an address book of Persons, each of which has multiple Phones. It is, in fact,
implemented in terms of Hibernate, using the tapestry-hibernate module.
</p>
<img src="ajaxformloop.png"/>
<subsection name="Person.java">
<source><![CDATA[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;
}
}
]]></source>
</subsection>
<subsection name="PhoneType.java">
<source><![CDATA[package org.example.addressbook.entities;
public enum PhoneType
{
HOME, OFFICE, MOBILE, FAX, PAGER
}
]]></source>
</subsection>
<subsection name="Phone.java">
<source><![CDATA[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;
}
}
]]></source>
<p>Notice that the number field is nullable but required. This is because, when creating a new Phone
instance, we have no number to fill in.
However, a number is expected, and the user interface enforces that.
</p>
</subsection>
<subsection name="Edit.tml">
<source><![CDATA[<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" 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>]]></source>
<p>
Here we're editing the direct properties of the Person object and adding a section below to allow
the phones for the person to be edited. The AjaxFormLoop looks much like a Loop component here,
except we must provide a PrimaryKeyEncoder object.
</p>
<p>
Each row provides a RemoveRowLink component that will remove that row (from the server side, then on
the client side).
</p>
<p>
The AjaxFormLoop provides a default row for adding additional data rows.
</p>
</subsection>
<subsection name="Edit.java">
<source><![CDATA[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;
@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);
}
}
]]></source>
</subsection>
<p>
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).
</p>
<p>
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.
</p>
<p>
AjaxFormLoop has to determine how to store an id for each editable row (remember that the client
side can only really store strings, not full Java objects); in some cases you will
have to bind the encoder parameter to a ValueEncoder object that is specific for your data type.
However, this is not necessary for any Hibernate entities, as Tapestry automatically provides
th ValueEncoder.
</p>
</section>
</body>
</document>