Tapestry is a component based framework. Examples of this come with the framework such as TextField which will render and keep the values of a user text entry field and a more complex ones such as the DatePicker that displays a text entry with a popup Javascript calendar. Components can include as many other components as you like, each having their own layout and Java code. Everything in Tapestry is said to be a component so where do pages fit in? If you think about it for a moment pages are just special components...simple as that.
Templates are in the Web project under src\main\webapp
Page objects are in the Java project that backs the web project under src\main\java\PACKAGE_NAME
As the cost of creating a page is high they are lazily created and placed in a pool of pages for faster subsequent use.
The Tapestry engine will call certain events (methods) on your page during its life cycle as shown here in this psuedo-example of an Edit page that gets some data from the persistance layer displays it to the user, lets the user change values and submit the results back to the database.
First we'll take a look at a the CrudList page that will display values from a Java ArrayList.
<body jwcid="border" title="CRUD List"> <span jwcid="@CrudResults"></span> </body>
Thats it? But how does it display anything with 2 lines?
This is a demonstration of the power of components. This template uses 2 custom components, "border" and "CrudResults". Tapestry components are declared within HTML by the special attribute "jwcid" (Java Web Component Id) You can use a named component who's use has been declared elsewhere in the case of the border. The "title" attribute is used by the Border component to set the display title of the page. The border component was declared in the Java page (actually the Base Page):
@Component(type = "Border") public abstract Border getBorder();
The CrudResults component is anonymous, we don't give it a name, Tapestry will for us. You usually would only name a component when you need to refer to it again. The syntax with the '@' character will declare the use of a component. You can combine the 2 methods to set a named component inline such as "myCrudList@CrudResults".
The use in this case may seem a little inconsistent but it acheives 2 things. This is the only page that uses the CrudResults component so it can be anonymous whereas the border is declared in the Base Page so all site pages can use it (thus giving us a nice consistent look and feel); and it demonstrates the component declaration techniques :)
The border component gives us a consistent, write-once use-many look and feel to all pages that use it...too easy. For this site we have a header, footer, left-side nav bar and a main content part where the results will be listed. So all our pages care about is filling up the content area and this is where the CrudResults component comes in.
The CrudResults component really is just a big block of HTML with Tapestry component uses, nothing too special (or necessarily well designed for that matter, it could be broken down into sub-components itself).
Looking deeper we see things in the template such as:
<a jwcid="@DirectLink" listener="listener:firstPage" parameters="ognl:visitObject.crudMetaInfo" disabled="ognl:firstPage" class="buttonHover"> << First Page </a>
Tapestry will render a HTML hyperlink for us with all the parameters encoded and when it is clicked it will call a method called "firstPage" in our Java page. The "ognl" stands for Object Graph Notation Language which is a way of directly access objects from the Java page that sits behind the template. So in a way the template and page are glued together, each can access each others objects. The call "visitObject.crudMetaInfo" is equivalent in Java code to "getVisitObject().getCrudMetaInfo()".
More on OGNL can be found in the OGNL language guide at http://www.ognl.org/
The listener and parameter match up directly to a method in our Java page that will be called when the user clicks on the link (if you don't match them up Tapestry will let you know all about it). The method in this case looks like:
public IPage firstPage(CrudMetaInfo crudMetaInfo) { this.setCurrentPage(0); log.info("Moving to FIRST page..."); return getPage(); }
To matchup the method name must equal the name given in the "listener:" and the parameter type must equal the "parameters:" type. Yes you can provide multiple parameters to the component (I think comma separated). The other interesting thing here is that the method returns a Tapestry type of IPage. If you return a page object Tapestry will display that page to the user, you can call setters on the page first and Tapestry will encode them all behind the scenes and pass them along; nice, safe and easy.
There are many more components that work in a similar way included in the Tapestry framework, this also gives way to other people writing and packaging components for others to use (see the Contrib library). All are documented here: http://jakarta.apache.org/tapestry/tapestry/index.html under Framework - Components and Contrib Library - Components.
The Java page must populate a list and then the template will use the "For" component to iterate over the list.
public abstract class CrudResults extends CrudBaseComponent implements PageBeginRenderListener { ..... // Holds the results that the template will iterate over public abstract List getResults(); public abstract void setResults(List results); // At each iteration this param will hold the current value from the above list public abstract Object getCurrentRow(); public abstract void setCurrentRow(Object obj); // Page event triggered by the Tapestry engine // This is a good place to populate our results to iterate over public void pageBeginRender(PageEvent event) { ....... if (getResults() == null) { // Grab results via Cayenne SelectQuery query = new SelectQuery(getVisitObject().getCrudMetaInfo().getJavaClass()); query.setPageSize(getVisitObject().getCrudMetaInfo().getPageSize()); setResults(getDataContext().performQuery(query)); } ....... }
Beyond the embedded comments this needs some explaination. Pages and components and their parameters (the abstract getters and setters) are best declared abstract. This way Tapestry can manage the object properly; really this is because pages are pooled for efficiency and the property values must be correctly reset otherwise one user might get a pooled page that had values left over from the last user (when the page was returned to the pool). Tapestry will create the actual subclass implementation at runtime.
The template iterates over the results:
....... <span jwcid="@For" source="ognl:results" value="ognl:currentRow" index="ognl:rowIndex"> <!-- Simplification of the actual code --> <span jwcid="@Insert" value="ognl:currentRow"/> .......
Notice again how each of the "ognl:" relates to a "getter" on the related Java page. Eg: "ognl:results" links to "public abstract List getResults()". Simple eh?
The value of the currentRow is printed out with the @Insert component. Around this we can put any kind of HTML to make it nice and pretty.
Take a look at another cut-down code segment:
<form jwcid="@Form" cancel="listener:cancelChanges" method="POST"> <table> <tr jwcid="@Any" class="ognl:beans.rowClass.next" element="tr"> <td> <span jwcid="@Insert" value="ognl:currentCrudAttribute.displayName"></span> </td> <td> <span jwcid="@Insert" value="ognl:currentCrudAttribute.value"></span> <span jwcid="@RadioGroup" selected="ognl:currentCrudAttribute.value"> <input jwcid="@Radio" type="radio" value="ognl:true"/>True/Yes <input jwcid="@Radio" type="radio" value="ognl:false"/>False/No </span> </td> <td> <span jwcid="@DatePicker" value="ognl:currentCrudAttribute.value"/> </td> <td> <span jwcid="@TextField" value="ognl:currentCrudAttribute.value" translator="translator:number"/> </td> <td> <span jwcid="@TextField" value="ognl:currentCrudAttribute.value" translator="translator:number,pattern=#.#"/> </td> <td> <span jwcid="@TextField" value="ognl:currentCrudAttribute.value"/> </td> </tr> <tr><td><input jwcid="@Submit" type="submit" value="Save Changes" action="listener:saveChanges" parameters="ognl:crudObject"/></td>
Just like with HTML to submit values in Tapestry you need a form, and wrapped inside the form you can have all types of fields such as text, radio buttons and check boxes. Tapestry takes this a step further and allows you to write your own and even comes bundled with a few such as the DatePicker. All the form fields once again map back to the Java page using OGNL. Then when the form is submitted the values are automatically wired up to the Java page parameters. In this example they all point to the same parameter, in the real code each component above is wrapped with @contrib:Choose and @contrib:When components that decide based on the attribute value which component type to display. So if the attribute type from the database/Cayenne is a Date then a DatePicker is displayed.
public abstract class CrudAddModify extends CrudBasePage { ....... public abstract CrudAttribute getCurrentCrudAttribute(); public abstract void setCurrentCrudAttribute(CrudAttribute crudObjectBeanAttribute); @Bean public abstract EvenOdd getRowClass(); .......
This is the same as when the DirectLink was clicked with the addition that all the form fields are automatically wired up to the corresponding Java page parameters.
<input jwcid="@Submit" type="submit" value="Save Changes" action="listener:saveChanges" parameters="ognl:crudObject"/>
upon clicking the submit button will call the corresponding Java page method:
public IPage saveChanges(CrudObjectBean crudObjectBean) { ....... // Save the Cayenne object etc... // Return to the list page getCrudListPage().setMsg("Changes saved..."); return getCrudListPage(); ....... }
I'll explain the mechanism for commiting data object changes later. For now I'll explain again how to return to another page:
To inject the page...easy:
@InjectPage(value = "CrudList") public abstract CrudList getCrudListPage();