560 likes | 730 Views
Tapestry Components for Web 2.0. Howard Lewis Ship TWD Consulting, Inc. hlship@gmail.com. What is Tapestry?. Application. Page. Page. Component. Component. Component. Component. Component. Component. Controller. View. Model. Java Beans. HTML template. Dynamic HTML output.
E N D
Tapestry Components for Web 2.0 Howard Lewis Ship TWD Consulting, Inc. hlship@gmail.com
What is Tapestry? Application Page Page Component Component Component Component Component Component
Controller View Model Java Beans HTML template Dynamic HTML output Links / Form Submissions
Border.html <div jwcid="loginDialog"> <div class="dialog"> <form jwcid="form"> <p> Enter your email address and password to log in. </p> <span jwcid="errors@Errors"/> <label jwcid="@FieldLabel" field="component:email"/> <input jwcid="email" size="30"/> <label jwcid="@FieldLabel" field="component:password"/> <input jwcid="password" size="30"/> <input type="submit" value="Login"/> <input jwcid="@Cancel" ajax="true"/> </form> <p> Not registered yet? <a jwcid="register2"> Click here to setup an account</a>. </p> </div> </div>
Border.html <div jwcid="loginDialog"> . . . </div> • Placeholder for tacos:Dialog component • Dialogs are invisible until un-hidden • Active dialogs mask the rest of the page
Border.jwc <component id="loginDialog" type="tacos:Dialog"> <binding name="hidden" value="dialogHidden"/> </component> • Dialog visibility from dialogHidden property of page • Login link: • Set dialogHidden to false • Re-render just the loginDialog
Border.jwc • Invoke the listener method • Re-render just the loginDialog component <component id="login" type="tacos:AjaxDirectLink"> <binding name="listener" value="listener:doShowLogin"/> <binding name="updateComponents" value="{ 'loginDialog' }"/> </component>
Form component • Tapestry forms are inside a Form component: • Unique names & ids for fields (even inside loops) • Tracking of user input and errors • Handling submit, refresh & cancel • AjaxForm: Partial page refreshes <form jwcid="form"> . . . </form>
Form Component • listener: Name of listener method to invoke • delegate Object that tracks user input and errors <component id="form" type="tacos:AjaxForm"> <binding name="updateComponents" value="{ 'errors' }"/> <binding name="success" value="listener:doLogin"/> <binding name="cancel" value="listener:doCancel"/> <binding name="delegate" value="delegate"/> </component>
OGNL • Object Graph Navigation Language • Expression language used by Tapestry • ognl: prefix when used in HTML template • No prefix when used in XML file
OGNL • Simple: property namesdelegate getDelegate(), setDelegate() • Complex: property pathspoll.title getPoll().getTitle(), getPoll().setTitle() • Much more!
TextField Component • @FieldLabel anonymous component of type FieldLabel • Nothing in the Login.page file • component:email reference to email component <label jwcid="@FieldLabel" field="component:email"/>: <input jwcid="email" size="30"/>
TextField Component • value property to read and update • validators validations to perform • displayName Used in error messages, and by FieldLabel <component id="email" type="TextField"> <binding name="value" value="email"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="message:email-label"/> </component>
Password TextField <component id="password" type="TextField"> <binding name="value" value="password"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="message:password-label"/> <binding name="hidden" value="true"/> </component>
Register Link <p>Not registered yet? <a jwcid="register">Click here to setup an account</a>. </p> • literal: value is just a string, not an OGNL expression <component id="register" type="PageLink"> <binding name="page" value="literal:Register"/> </component>
Java Class public abstract class Border extends BaseComponent { public abstract String getEmail(); public abstract String getPassword(); . . .
Abstract? • Pages are stateful • Hold transient data during request • Hold persistent data between requests • Pages are expensive to create • Pages are pooled • Like database connections
No, Really, Abstract? • Tapestry extends abstract class • Adds getter, setter, instance variables • Adds end-of-request cleanup • Lots of injections based on getters and annotations (or XML)
Java Class public abstract class Border extends BaseComponent { public abstract String getEmail(); public abstract String getPassword(); . . . • getEmail() & setEmail() • getPassword() & setPassword()
Listener Methods • Public method • Changes server side state • Form will re-render public void doCancel() { getLoginDialog().hide(); }
Injecting Services • Service defined in HiveMind IoC Container • Can inject Spring beans as easily • Keep business logic out of pages / components @InjectObject("service:epluribus.LoginAuthenticator") public abstract LoginAuthenticator getAuthenticator(); public interface LoginAuthenticator { User authenticateCredentials(String email, String plaintextPassword); }
Listener Methods public String doLogin() { String email = getEmail(); String password = getPassword(); User user = getAuthenticator().authenticateCredentials( email, password); . . .
Listener Methods . . . if (user == null) { getDelegate().record(null, "Invalid user name or password."); return null; } getIdentity().login(user); getLoginDialog().hide(); }
Server Side State: ASO @InjectState("identity") public abstract Identity getIdentity(); • Application State Objects • Global to all pages • Stored in HttpSession • Created on demand • Defined in HiveMind • Injected into pages or components
Identity ASO public class Identity implements Serializable { . . . public boolean isLoggedIn() { . . . } public void login(User user) { . . . } public void logout() { . . . } public User getUser() { . . . } }
Home.html <table class="data-grid" cellspacing="0" jwcid="polls"/> <div jwcid="pollingEndColumnValue@Block"> <span jwcid="@InsertDate" date="ognl:poll.pollingEnd"/> </div> <div jwcid="statusColumnValue@Block"> <span jwcid="@Insert" value="ognl:responseCount"/> <a jwcid="respond">Respond</a> </div>
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component>
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • Defines a new property on page • Alternately: create abstract property
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • contrib: is name of tapestry-contrib.jar library
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • source total list of Poll objects
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • columns how to break a Poll object into columns
Home.properties • column-id : title : OGNL expression • poll.title • poll.questionCount • null calculated elsewhere • !status status is not sortable table-columns=\ title:Title:title, \ questions:Questions:questionCount, \ pollingEnd:End of Polling:pollingEnd, \ !status:Status:null
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • row Property to update with each rendered row (each Poll)
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • index Stores index into row (used to set row CSS class)
Home.page <property name="rowIndex"/> <component id="polls" type="contrib:Table"> <binding name="source" value="polls"/> <binding name="columns" value="message:table-columns"/> <binding name="row" value="poll"/> <binding name="index" value="rowIndex"/> <binding name="rowsClass"> rowIndex == 0 ? "first" : null </binding> </component> • rowsClass CSS class value for the <tr> • Identify first row to change its formatting
Home.html <table class="data-grid" cellspacing="0" jwcid="polls"/> <div jwcid="pollingEndColumnValue@Block"> <span jwcid="@InsertDate" date="ognl:poll.pollingEnd"/> </div> <div jwcid="statusColumnValue@Block"> <span jwcid="@Insert" value="ognl:responseCount"/> <a jwcid="respond">Respond</a> </div>
Home.html <table class="data-grid" cellspacing="0" jwcid="polls"/> <div jwcid="pollingEndColumnValue@Block"> <span jwcid="@InsertDate" date="ognl:poll.pollingEnd"/> </div> <div jwcid="statusColumnValue@Block"> <span jwcid="@Insert" value="ognl:responseCount"/> <a jwcid="respond">Respond</a> </div>
Home.page • DirectLink invokes a listener method when clicked • Can pass parameters into the listener method <component id="respond" type="DirectLink"> <binding name="listener" value="listener:doRespond"/> <binding name="parameters" value="poll.id"/> </component>
Home.java • Parameters show up … with proper type (not just String) @InjectPage("RespondToPoll") public abstract RespondToPoll getRespondToPoll(); public void doRespond(long pollId) { Poll poll = getPollAccess().getPoll(pollId); // TODO: A few checks, i.e., Poll is active getRespondToPoll().activate(poll); }
FCKEditor • "the text editor for the Internet" • Open Source • http://www.fckeditor.net/
FCKEditor • Primarily a JavaScript library: • FCKeditor/fckeditor.js • Goal: • Component to take place of TextArea
FCKEditor Component • FCKEditor.jwc Copy of TextArea.jwc • FCKEditor extends TextArea
FCKEditor.java protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) { super.renderFormComponent(writer, cycle); // Now, we want to work with the script. PageRenderSupport support = TapestryUtils.getPageRenderSupport(cycle, this); support.addExternalScript(getEditorScript().getResourceLocation()); String contextPath = getRequest().getContextPath(); String id = getClientId(); String clientObject = "editor_" + id; StringBuffer buffer = new StringBuffer(); buffer.append(String.format("var %s = new FCKeditor('%s');\n", clientObject, id)); buffer.append(String.format("%s.BasePath = '%s/FCKeditor/';\n", clientObject, contextPath)); buffer.append(String.format("%s.ReplaceTextarea();\n", clientObject)); support.addInitializationScript(buffer.toString()); }
FCKEditor.java super.renderFormComponent(writer, cycle); PageRenderSupport support = TapestryUtils.getPageRenderSupport(cycle, this); support.addExternalScript( getEditorScript().getResourceLocation()); @Asset("context:FCKeditor/fckeditor.js") public abstract IAsset getEditorScript();
FCKEditor.java String contextPath = getRequest().getContextPath(); String id = getClientId(); String clientObject = "editor_" + id;
FCKEditor.java StringBuffer buffer = new StringBuffer(); buffer.append(String.format( "var %s = new FCKeditor('%s');\n", clientObject, id)); buffer.append(String.format( "%s.BasePath = '%s/FCKeditor/';\n", clientObject, contextPath)); buffer.append(String.format( "%s.ReplaceTextarea();\n", clientObject)); support.addInitializationScript(buffer.toString());