Just got back from a three day Tapestry training session with Jordan, Matt and the crew at shopping.com out in San Francisco. This was a fun, exhausting trip but everyone (myself included) got a lot out of it. Things I learned:
- Be emphatic. The labs are good, even if they appear tedious. Nobody wanted to do them, and everyone was glad that they did.
- Let Matt explain the problem before giving him the (wrong) solution.
- Have a template Tapestry/Intellij project ready to go.
- Flash memory keys are cool (we passed lots of files around, without having to get involved with network setup).
Along the way, we solved (to a degree) some problems with input validation (in Tapestry 3.0). If you've used validation, you know that you can get field labels to be decorated, along with the fields themselves. However, when using a loop around the input fields, there's an off-by-one error that causes the wrong label to be decorated.
The problem is that the FieldLabel component relies on the ValidField having the correct value in its name
propery when the FieldLabel renders, so that it can work with the validation delegate to determine if and how to decorate the label. However, the name
property isn't set until the ValidField renders itself (that's when it obtains its name from the Form component, and registers itself with the validation delegate as the active component).
So if the FieldLabel and the ValidField are in a loop that executes three times, and the 2nd rendering of the ValidField is the one that's in error, its the 3rd rendering of the FieldLabel that gets decorated. Woops.
So, the trick is ... we need to render the ValidField before the FieldLabel, but the output from that rendering must still occur after the VAlidField renders. Why that output order? Because that's that's how typical, western language forms are output, with FieldLabels rendering first (and thus, to the left of) ValidFields.
So, at the customer site, I created a component I call Defer. Defer takes a Block as a parameter. It renders the Block into a buffer, then renders its body, then outputs the buffered content from the Block. Something like:
public abstract Block getBlock(); protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) { IMakrupWriter nested = writer.getNestedWriter(); getBlock().renderBody(nested, cycle); renderBody(writer, cycle); nested.close(); }
How is this used? The FieldLabel is enclosed inside a Defer, and the ValidField is enclosed inside a Block.
<span jwcid="@Defer" block="ognl:components.fieldBlock"> <span jwcid="@FieldLabel" field="ognl:components.inputName"/>: </span> <span jwcid="fieldBlock@Block"> <input jwcid="inputName" ... /> </span>
So the inputName component renders first, then the FieldLabel renders, then the HTML from the inputName component is output. This is once of the many things I love about Tapestry ... its not just a stream of text, its actual objects. Once you see past the false reality of a text stream to the Matrix of the Tapestry component object model, you are free to twist time (or at least, control the order of rendering).
It's far from perfect ... as with using a ListEdit/ListEditMap combination inside a Form, it forces too much of Tapestry's internals into your lap ... but it is workable, and it is only needed for special cases where a ValidField component's name changes while the form renders -- which is to say, only when using the FieldLabel/ValidField combination inside a loop.
Mikael, perhaps you could explain what you mean at length somewhere (it isn't obvious to me), either on tapestry-dev mailing list, or on the Wiki. I can reasonably defend why Tapestry works the way it does in a more interactive forum.
ReplyDeleteIs there an equivalent for Tapestry 5?
ReplyDeleteIt's not needed for Tapestry 5; the two components can coordinate using the Heartbeat environmental. That being said, I know that Ioko has a component that's somewhat similar, but more about moving the rendered content elsewhere in the DOM before streaming.
ReplyDelete