Helma Logo
main list history

Handler for rendered skins

While developing the first version of Rabbit a little idea came into my mind which suddenly opened a door for even more comfortable skin rendering.

I added a method called render() which mainly is wrapping renderSkin(). However, it takes a string as third argument:

 function render(skin, param, str) {
    renderSkin(skin, param);
 }

This string is used as name for a property of res.handlers.rendered
the rendered skin will be assigned to:

 function render(skin, param, str) {
    if (str) {
       var result = this.renderSkinAsString(skin, param);
       res.handlers.rendered[str] = result;
       return result;
    } else {
       renderSkin(skin, param);
    }
 }

Also note that the rendered skin is returned as result if the third argument is set making a notorious "renderAsString()" sibling obsolete.

This way, I now can do the following with render():

 // directly output a rendered skin as usual
 render("foo", param);
 // assign a rendered skin as string to a variable
 var bar = render("foo", param, "foobar");
 // use the "rendered" handler as container of previously rendered skins
 render(createSkin("<% rendered.foobar %>")); 
 // displays contents of res.handlers.rendered.foobar 
 // (which is the same as bar above)

By cascading render calls, each with a third argument, it's very easy to build structures:

 render("header", null, "one");
 render("main", {name: "World"}, "two");
 render("footer", null, "three");
 render("page");

The skins:

 <!-- header.skin -->
 <html>
 <title>Test</title>
 <body>
 <!-- main.skin -->
 Hello, <% param.name %>!
 <!-- footer.skin -->
 </body>
 </html>
 <!-- page.skin -->
 <% rendered.one %>
 <% rendered.two %>
 <% rendered.three %>

The above code virtually cries for dropping the third argument again and using the first (the skin name) as the name for the res.handlers.rendered property. However, then we need a different method to indicate that a skin should be rendered as string and to prevent every rendered skin being assigned to a res.handlers.rendered property...

Of course, many improvements can be made from here. What I did in a final step is to append rendered skins if the third argument specifies a property already defined in res.handlers.rendered:

 function render(skin, param, str) {
    if (str) {
       var result = this.renderSkinAsString(skin, param);
       if (!res.handlers.rendered[str]) {
          res.handlers.rendered[str] = "";
       }
       res.handlers.rendered[str] += result;
       return result;
    } else {
       renderSkin(skin, param);
    }
 }

Now it becomes really elegant to render a collection:

 render("header", null, "one");
 for (var i=0; i<this.size(); i+=1) {
    render("item", this.get(i), "list");
 }
 render("footer", null, "two");
 render("page");
 <!-- item.skin -->
 <a href="<% param.url %>"><% param.name %></a>
 <!-- page.skin -->
 <% rendered.one %>
 <% rendered.list %>
 <% rendered.two %>

Here, the only ugly thing is the missing HopObejct render() method and that we have to send the item object as argument into the global render() method. Just needs to be done. // tobi

Links to this page: Enhancements for response buffer handling

Comments

#1 by hannes at 2007/03/24 01:34

Very intriguing! I have tinkered with this, and have come up with a small but significant modification. Taking your idea to use the skin name as name for the rendered part, let's change the third argument to the object in which to store the parts:

  var parts = res.handlers.parts = {};
  renderPart("header", null, parts); // -> parts.header
  renderPart("body", {foo: "bar"}, parts);
  renderPart("footer", null, parts);
  renderSkin("page"); 

That way we also make this flexible, because we can render into res.data or any other object we wish, instead of hardcoding it to res.handlers.rendered.

The only problem I see is that we need to use the StringBuffers of the response object directly if we want the append feature and have it perform well. I'm currently thinking about ways to do this. Maybe a variant of res.push() that allows to pass a StringBuffer in, and a variant of res.pop() that returns a StringBuffer instead of a String.

#2 by hannes at 2007/03/24 13:33

or maybe: add an optional 4th argument to override the default name to store the rendered part:

  renderPart("#row", param, parts, "tablecontent"); // -> parts.tablecontent

... would also allow to use skin objects directly.

#3 by tobi at 2007/03/24 22:08

Lucky us that I passed by here by chance today...

I like your modifications. renderPart() is a good name, as good as it is to allow choosing a target object to render the parts into.

The next thing which strikes me then is that maybe we should swap the second with the third argument, just because the param object now seems to be the least required one.

  renderPart("header", parts); // spare the "null"
  var s = renderPart("footer", null, {foo: "bar"}); // although...

Regarding the StringBuffers problem you describe at the end of comment #1: do you mean to avoid the += operator on the string?

#4 by hannes at 2007/03/25 13:15

Acutally, if you see the parameter sequence as (skinid, inputobj, outputobj, [outputid]), I think I slightly prefer the original argument order.

But let me elaborate a bit on the param object: I think the reason that the renderSkin param object has played a marginal role in Helma so far is tightly coupled with the fact that its properties had to be prefixed with param in skins and that the maximal depth of accessing properties in the param object was 1. Thus, everything you could pack in the param object had to be pretty much pre-rendered strings. (Same is true for all other handlers, btw: either macros, or prerendered strings. It'll take some time to fully realize that this is no longer true.)

Deep macro paths help a bit here, because you could invoke a macro with a path like param.story.abstract, so the param object could for the first time be useful for cases where we don't know exactly what to push to the macro.

But I'm thinking about a further change.

Since one month or so, the handling of the renderSkin param object has changed from passing it through internally to just registering it as res.handlers.param before rendering the skin, and resetting res.handlers.param to the old value afterwards (this is done with a try/finally statement in helma.framework.core.Skin.render()). To me this make just sense, it feels logical and natural, and embedded macros can now access the renderSkin param object in res.handlers.param.

Yesterday I had the idea of taking this concept one step further. Instead of just registering the param object as res.handlers.param, we could take the contents of the param object and register each property in res.handlers. This means that passing a param object to renderSkin would be equivalent to register each of its properties in res.handlers, and unregistering it afterwards.

With this, the param argument would be promoted from unloved/underused feature into a real powertool, as it would replace manually setting res.handlers, with the added bonus of automaticall restoring the previous state of res.handlers after the skin has finished rendering. Of course there's a little flaw in all this, which is that it's not backwards compatible, so I guess the behaviour would need to be switchable.

#5 by tobi at 2007/03/26 20:03

I added a comment to the above here.

If possible I'd like to focus back on the renderPart() method. I still would like to know more about the StringBuffer issue.

#6 by hannes at 2007/03/26 20:22

Yes, the StringBuffer is to avoid the += for string concatenation. What we want is to use the exact StringBuffer instance each time we append to a rendered part, so we need a way to get StringBuffers in and out to/form helma.framework.ResponseTrans. I have this implemented in my working copy of Helma adding res.pushBuffer(StringBuffer) and res.popBuffer() methods.

#7 by hannes at 2007/03/27 15:01

I just committed to new res-methods (StringBuffer stands for java.lang.StringBuffer):

 StringBuffer res.pushBuffer(StringBuffer) 
 StringBuffer res.popBuffer()

They behave similar to res.push/pop, but pushBuffer takes a StringBuffer argument (which may be null), and popBuffer returns the StringBuffer without converting it to a String. pushBuffer also returns the new StringBuffer, which may be useful in case the argument was null.

With this, a simple renderPart implementation looks like this:

 function renderPart(skinId, paramHolder, resultHolder, resultId) {
     if (resultHolder) {
         resultId = resultId || skinId;
         res.pushBuffer(resultHolder[resultId] || null);
         renderSkin(skinId, paramHolder);
         resultHolder[resultId] = res.popBuffer();
     } else {
         renderSkin(skinId, paramHolder);
     }
 }