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
Comments
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:
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.
or maybe: add an optional 4th argument to override the default name to store the rendered part:
... would also allow to use skin objects directly.
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.
Regarding the StringBuffers problem you describe at the end of comment #1: do you mean to avoid the += operator on the string?
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.
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.
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.
I just committed to new res-methods (StringBuffer stands for java.lang.StringBuffer):
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: