Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Sunday, December 12, 2004

Back from shopping.com / Defer component

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.

4 comments:

Mikaƫl Cluseau said...

There's something I still don't understand after some time under Tapestry : why isn't there context mecanism ?

I mean, markup languages are all trees so the context paradigm is easily applicable and would put this kind of issues away, and everything would just work.

Or, maybe, separate the setup and render phases, which is quite close to what you did to solve the problem.

Howard said...

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.

Peter Stavrinides said...

Is there an equivalent for Tapestry 5?

Howard said...

It'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.