Recall that nearly everything in Tapestry is deemed a component. So lets take a look at some custom components.
The Border component is a great way to give your site a consistent look and feel. The name given to this component is a psuedo-standard from the Tapestry community for this type of functionality. In fact you could have any number of Border components to give your site a different look for different sets of functionality. For example one for the public site, one for a members area and one for the site administrators. There really is little you have to know for this special component. It has a HTML template that sets the layout and a minimalist Java class.
<span jwcid="shell"> <body jwcid="@Body"> <table border="0" cellspacing="0" cellpadding="0" width="90%"> <tr valign=top> <td colspan="2"> <!-- Top banner area --> <!--<img jwcid="@Image" image="ognl:bannerAsset" alt=""/>--> </td> </tr> <tr halign="left" valign=top> <td width="10%"> <!-- Left navigation panel, inserts another component that renders the menu --> <span jwcid="@CrudMenu"/> </td> <td valign=top> <!-- Render the body of each page that is wrapped by this component --> <span jwcid="@RenderBody"/> </td> </tr> <tr> <td> <!-- Uncomment to turn on the Tapestry inspector to find out runtime info about your page --> <!--<span jwcid="@contrib:InspectorButton"/>--> </td> <!-- Copyright notice --> <td class="copyright" height=4>Copyright (c) 2006 </td> </tr> </table> </body> </span>
The only real magic here is the RenderBody component. A page can only have one RenderBody component which renders the body of the page that this component is wrapping...ie. our page that calls this component.
/** Define this class a Component */ @ComponentClass(allowBody=true, allowInformalParameters=true) public abstract class Border extends CrudBaseComponent { // This component in turn calls the Tapestry Shell component so we can set a CSS @Component(type = "Shell", bindings = {"stylesheet=assets.styleSheet", "title=literal:crud"}) public abstract IComponent getShell(); }
Inject it into your page via the Component annotation:
@Component(type = "Border") public abstract Border getBorder();
I advise putting this into a Base class (somewhere in your object hierarchy) so that all pages that sub-class this will automatically have the same look and feel.
In your template you'll still need to reference the "border" and set the title of the page.
<body jwcid="border" title="CRUD List">
Lets say you have a number of pages (perhaps report pages) that all have the requirement to set what the output type (eg PDF, CSV, plain text) is when the page is run. Well in my mind this right away flags that multiple uses of the same funtionality should all go into the one component - write once, use many. Here I'll demonstrate how to create a reusable drop down might work that will capture what export type the user has selected.
As said all this component is, is a preset set of values displayed in a dropdown, so make use of the Tapestry PropertySelection which requires a model from which to get its display values for and a value that holds what the user has selected.
<span jwcid="exportSelector@PropertySelection" model="ognl:exportTypeSelectionModel" value="ognl:exportType"/>
This should be straight forward enough by now, all the values are holders for the values in the template. The added thing of note here is the ExportTypeSelectionModel which is another custom class that holds the values to display and ...
public abstract class ExportTypeSelector extends DMSBaseComponent { // Value user has selected public abstract MimeType getExportType(); public abstract void setExportType(MimeType mimeType); // Model of values static ExportTypeSelectionModel exportTypeSelectionModel; public ExportTypeSelectionModel getExportTypeSelectionModel() { if (exportTypeSelectionModel == null) { exportTypeSelectionModel = new ExportTypeSelectionModel(); } return exportTypeSelectionModel; } }
public class ExportTypeSelectionModel implements IPropertySelectionModel, Serializable { static MimeType[] mimeTypes = MimeType.values(); public int getOptionCount() { return mimeTypes.length; } public Object getOption(int index) { return mimeTypes[index]; } public String getLabel(int index) { String label = mimeTypes[index].getMimeTypeDescription(); return label; } public String getValue(int index) { return Integer.toString(index); } public Object translateValue(String value) { return getOption(Integer.parseInt(value)); } }
public enum MimeType implements Serializable { CSV("Csv", "text/csv", "Comma separated values", ".csv"), PDF("Pdf", "application/pdf", "Adobe PDF", ".pdf"), EXCEL("Excel", "application/excel", "MS Excel", ".xls"), XML("Xml", "text/xml", "XML", ".xml"), TXT("Text", "text/plain", "Plain Text", ".txt"); private String mimeType; private String webHeaderString; private String mimeTypeDescription; private String fileExtension; private MimeType() { // Dont allow empties } MimeType(String mimeType, String webHeaderString, String mimeTypeDescription, String fileExtension) { this.mimeType = mimeType; this.webHeaderString = webHeaderString; this.mimeTypeDescription = mimeTypeDescription; this.fileExtension = fileExtension; } public String getMimeType() { return mimeType; } public String getWebHeaderString() { return webHeaderString; } public String getMimeTypeDescription() { return mimeTypeDescription; } public String getFileExtension() { return fileExtension; }
<tr> <td>Export type:</td> <td><span jwcid="exportTypeSelector@ExportTypeSelector" value="ognl:exportType"/></td> </tr>
public abstract MimeType getExportType(); public abstract void setExportType(MimeType mimeType);
To stream data to back to a client requires the use of a service. This example will be continued under the Service section.