Skip to content

Instantly share code, notes, and snippets.

@sdesai
Created November 22, 2011 20:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sdesai/1386836 to your computer and use it in GitHub Desktop.
Save sdesai/1386836 to your computer and use it in GitHub Desktop.
WidgetStringRenderer

USE CASES

  • Render widgets on NodeJS, where DOM is absent.
  • Optimize rendering of Widgets at scale (1000s of TreeViewNodes).

GOAL

  • Avoid all Node references from Widget through the end of renderUI().
  • bindUI()/syncUI() will still have Node references (to bind events and incrementally update DOM).

DESIGN

Opt-in Widget Extension, to maintain backwards compatibility.

A boundingBox/contentBox Node instance won't be present for initializer/attr setters/attr getters/HTML_PARSER, so needs to be opt-in.

  1. Component Developer can opt-in, if they have a 100% string rendered component (e.g. TreeViewNode)

    In this case, get("boundingBox") etc. needs to be documented accordingly for the end user.

  2. Environment can opt-in (e.g. NodeJS - conditional loading)

Example:

Y.WidgetStringRenderer = function() {...};

Y.WidgetStringRenderer.prototype = {
   // override what needs to be overridden in Y.Widget to support string template based rendering.
}

Y.Foo = Y.Base.create("foo", Y.Widget, [Y.WidgetStringRenderer]);

Handlebars Default

It'll use Handlebar style templates as opposed to substitute, for forward compatibility.

However we should maybe look into a "handlebars-core" to satisfy the kweight nitpickers. We've been asked to break out less than 3KB chunks before which is where handlebars-base-min.js currently is.

"handlebars-core" could provide basic {{ }} and {{{ }}} support, and also maybe provide substitute compatibility (how to identify single { tokens, from content in a handlebars template?).

Two Phase Render

We'll need to break up the render() phase, into the renderUI() portion and the bind/syncUI() portion

render()
    renderUI() : No Node references
    bindUI()   : Node references
    syncUI()   : Node references

Not sure what the method split should be yet. Options are below, first one is my leading candidate

Options

TreeView use case:

// While iterating 1000s treeview nodes ...
treeviewNode.renderHTML(buffer);     // only renderUI() - is it OK that render event is not fired? 
                                     //                   I think so. Nothing is in the DOM yet.

// Once injected into the DOM ...
treeviewNode.render();         // renderUI() [if not invoked before], bindUI(), syncUI()

NodeJS use case:

// On Server
calendar.renderHTML();

// On Client
calendar.render();

Or,

treeviewNode.render();
treeviewNode.bind();        // How about if they want to do it all at once in render(); bind() and Y.bind() confusion

Or,

treeviewNode.renderHTML();
treeviewNode.bind();

Or,

treeviewNode.renderUI() // When do we fire/set render state?
treeviewNode.bindUI()
treeviewNode.syncUI()

Will require re-establishing boundingBox/contentBox Node references, post-renderUI().

Widget will generate node instance from rendered template content for boundingBox, contentBox

Usage

TreeViewNode - Always 100% String rendered

Y.TreeViewNode = Y.Base.create("treeViewNode", Y.Widget, [Y.Parent, Y.Child, Y.WidgetStringRenderer]);

// In Parent.render() ...
var buffer = [];
for (i = 0; i < children.length; i++) {
    child.renderHTML(buffer);   
}

var allChildrenHTML = buffer.join("");

Only on NodeJS (conditionally loaded extension mix)

// calendar-nodejs, or maybe just widget-base-nodejs
Y.Calendar = Y.Base.create("calendar", Y.Widget, [Y.WidgetStringRenderer]);

var buffer = []
calendar.renderHTML(buffer);

var calendarHTML = buffer.join("");

We can add sugar in the future (render straight into a template for example). Not enough time for Sprint 1.

calendar.renderHTML(template, token);
@rgrove
Copy link

rgrove commented Nov 23, 2011

Thoughts:

  • Anything that isn't Handlebars shouldn't be called Handlebars. That's just too confusing. If you need something simpler than Handlebars but not as simple as Y.Lang.sub or Y.substitute, it should be a standalone module with a distinct name, no matter how similar the syntax looks to Handlebars. A primary goal of Y.Handlebars is to maintain 100% template compatibility with upstream Handlebars, to avoid locking people into a YUI "flavor" of Handlebars that doesn't quite work with other flavors.
  • The entire purpose of handlebars-base vs. handlebars-compiler is so that in performance- or byte-sensitive environments, templates can be precompiled on the server or in a build step and then the only dependency needed to render them is handlebars-base, which is a mere 1.2KB after min+gzip. Why do we need to create a new category of template parser and introduce fragmentation? Is 1.2KB really too big?
  • Conceptually, there's a lot of overlap here with View, and there will be even more overlap once we start introducing data binding features in the form of View subclasses/extensions. If for some reason basing WidgetStringRenderer on View is not an option, there needs to be a very clear story for why a developer should use one vs. the other.

If the goal is to optimize cases like rendering thousands of TreeViewNodes, then it seems suboptimal to start with a strongly mapped, instance-based parent/child hierarchy in which each child is a Widget instance and is responsible for rendering itself.

I think the ideal case would be for the parent to be capable of rendering all the children with a single render call (best performance for an initial render) and for individual children to be able to render themselves on demand (best performance for incremental changes). This could be done by designing the templates such that the parent's template includes the child template as a partial. Then it can render everything at once while only incurring the spinup cost for a single Handlebars VM context, or it can call a child's render method to render a subset.

An additional benefit is that when the parent performs the initial render, all it needs is the data for the children. It doesn't technically need an actual heavyweight instance of each child. This opens up possibilities for flyweight patterns that would increase the efficiency of working with thousands of children.

@sdesai
Copy link
Author

sdesai commented Nov 23, 2011

Thanks for the feeedback.

Further discussion below...

  • I agree we should stick with handlebars-base - and that is the plan, as mentioned.

I personally don't have a k-weight nitpick. We've seen pushback from (the usual) customers in the past, looking to cut a couple of KB here, and a couple there, which was the only thing behind the thought I had. I agree, that it's worth pushing back for the sake of keeping the imported Handlebars pure.

  • Can you expand on the overlap with View? I don't see it.

The goal of this extension is to provide a version of Widget's base rendering API which is not Node dependent, either for use on NodeJS, or to help optimize rendering at scale.

I don't see it using any of the current Y.View API, or an explicit Model binding.

  • I think the flyweight use case applies either way.

We've decided (in prior TreeView design discussions) that there's value in the Parent/Child Widget API as it exists today, for TreeNodes.

Given that decision, there are 2 optimizations which can help with performance.

a) Provide a purely off DOM, string based rendering path for Child Widgets.
b) Apply a flyweight pattern so we're not creating a full blown Widget instance for each TreeNode.

WidgetStringRenderer aims to address a) and b) is still on the table as an option down the road.

If we want to revisit that decision and change TreeNode to be something other than a Widget, then that's a different discussion, which we should reopen separately (and soon, although if it's not Widget based, we'll have Sprint 2 to implement it).

  • Regarding partial support - I agree, if there's performance gain in using a single render/context for the parent-child relationship, then we can look at building that into Parent/Child. Maybe leverage helpers? Do you know how big a performance factor the spinup cost is?

@lsmith
Copy link

lsmith commented Nov 24, 2011

I haven't read through entirely, and will likely have more feedback (meeting, perhaps?).

IMO, markup string composition is a good baseline approach and is useful in 2 of what I see as the 3 core "render"ing scenarios:

  1. in browser, progressively enhance existing DOM
  2. in browser, dynamically generate DOM
  3. not in browser, dynamically generate markup for delivery to browser

The distinction as you noted in the gist is whether it's appropriate to do any work beyond creating the markup. #1 gets no benefit from DOM or markup string composition at all unless mutation is supported, but needs the binding/syncing(optional?) step. #2 needs markup generation, DOM creation, and binding. #3 needs only markup generation.

I entertained the idea of having a markup factory for DataTable a while back, which DT would leverage, but would exist as a separate utility. I liked the separation of DOM generation from feature enhancement because it would allow implementers the ability to render a static table from a set of data without the overhead of Widget instance creation. The resulting table could then be enhanced with Node plugins to add features. In the end, I opted to go with View instances hung off the DT instance serving as renderers of the various table sections. This would offload the choice of #1, #2, or #3 to the given View assigned at instance creation, while preserving the class API. Because DT has a wide range of possible features that impact the markup/DOM, I suspect a class extension like StringRenderer would run into conflicts fairly quickly.

The result is similar given the DT's View/Renderer case, though. The default renderer (be it baked in, mixed in with extension, or delegated to a View) would need to be replaced with an alternate that supports the conflicting feature. Having the renderer be configurable, though, hopefully obviates the need for Base.create calls to add features or resolve rendering conflicts.

The resulting arch for DT is that the Views will be treated like markup factories, receiving the DT instance as the data origin. But theoretically, these Views would be able to live separately and serve as markup/DOM factories for static data.

I think this harkens back to your original thought of Widgets as MVC triplets with various renderers.

After this long stream-of-consciousness novella, I wonder if it would be preferable to have the DOM rendering implementation broken out into a class extension and WidgetBase would be an abstract class.

Gotta run--babies crying.

@lsmith
Copy link

lsmith commented Nov 24, 2011

renderHTML reminds me of Model's toHTML method.

@rgrove
Copy link

rgrove commented Nov 24, 2011

@sdesai: Regarding the overlap with View: what I mean is that, conceptually, View and WidgetStringRenderer would serve similar purposes. Out of the box, View requires a Node instance that it can use as a container, but this behavior is easy to customize via a subclass or extension, and the stage is already set for template-based rendering, not to mention future data binding support.

I don't see it using any of the current Y.View API, or an explicit Model binding.

View supports, but does not require, an explicit Model binding. It's perfectly reasonable to use a View without a Model.

I used the word "conceptually" because there's not much actual functionality inside View; it's more of a generic idea with broad potential, and one of the things it has potential for is something like this. The benefit is in the conceptual consistency of using View throughout the library as a low-level HTML rendering tool.

If we want to revisit that decision and change TreeNode to be something other than a Widget, then that's a different discussion, which we should reopen separately (and soon, although if it's not Widget based, we'll have Sprint 2 to implement it).

I don't think it's necessary to make a TreeNode something other than a Widget, but I think relying on per-instance rendering will introduce a performance barrier that can't be overcome no matter how much the Widget lifecycle code is optimized. If the parent/child relationship can be made flexible enough that the parent can render the children without having to instantiate and call into each one of them, then there's more potential for TreeView and other parent/child widgets to be able to handle huge numbers of children without bogging down or gobbling memory.

Regarding partial support - I agree, if there's performance gain in using a single render/context for the parent-child relationship, then we can look at building that into Parent/Child. Maybe leverage helpers? Do you know how big a performance factor the spinup cost is?

I don't have any numbers on the Handlebars spinup cost, but it's worth looking into. I expect it will be trivial compared to widget instantiation cost, but still worth avoiding if we can.

@lsmith: I really like the idea of Views being used as markup factories like you're planning for DataTable. That's definitely worth exploring further, and I'd be happy to enhance View (or create subclasses/extensions) to better serve this use case.

Regarding Model's toHTML() method: there's no such thing. Maybe you're thinking of getAsHTML(), which gets an HTML-escaped version of a model attribute?

@lsmith
Copy link

lsmith commented Nov 25, 2011

@rgrove Yep, my mistake. I remembered there being some to/as HTML method, but it seemed odd even as I wrote it that there would be a method on a Model for creating an HTML representation of the whole, since that is View's responsibility. HTML escaping for individual attributes, on the other hand, is fine for Model behavior.

@sdesai
Copy link
Author

sdesai commented Nov 28, 2011

@rgrove:

I see that conceptual value if Widget is broken up into separate M, V, C components as discussed in the App/Widget conversation. If I'm just mixing in support for string based rendering to the current Widget API, I don't see any value in pulling the View API along if we won't be using any of it, especially since it'll probably lead to confusing overlap with similar areas of Widget which are already defined (bounding box/content box templates vs. template, widget destruction vs. view destruction, model vs. widgets attrs, container vs boundingBox etc).

If we do end up breaking Widget into a separate M, V and C down the road (not currently in the 3.5.0 timeline, maybe 3.6.0) then I think it makes complete sense to see what the most basic M, V and C could be which could be used in the Widget world as well as the App world.

I'm leaning towards having WidgetStringRenderer be something that maybe Tree owns (I'll do the work, but it'll be an internal TreeViewNode impl) for the 3.5.0 release (or at least for the pr1 release), due to the shortness of time, and we can visit the Widget MVC breakup in more depth for 3.6.0 - which I believe is the real answer to the above design pros/cons.

I'll think about it a little further from the angle of delegating to a View string renderer, but I think there's too much backwards compatibility baggage to carry along and I'm likely to go with the above.

@lsmith, @rgrove:

I agree - when we don't have an existing API we need to reconcile (as in the base Widget case) doing whatever we can to break the view pieces of a widget out into encapsulated reusable chunks makes sense. I would argue though that View needs to be more basic, if we have more than a couple of use cases which don't end up being bound to a Model (that was the basis of my DT review question). Having for example a DT header View instance, which is not bound to a model, but has a model/modelList attribute, leads to some confusion, especially in the DT world. The most basic View could be purely a renderer (with associated utility methods/attributes), without any Model/Data binding overhead.

@lsmith:

Re: WidgetBase, while playing around with the Widget MVC breakout during the week of NodeJS, that's actually what I had - WidgetBase + WidgetLifecycle + WidgetView + WidgetModel + WidgetController extensions [ didn't get as far as putting it all back together ]

@lsmith
Copy link

lsmith commented Nov 28, 2011

@sdesai, Agreed that the Widget MVC breakup is probably the real answer to the above pros and cons.

While I can see the value in having a class extension that can be mixed into the existing arch while we revisit a larger project like breaking Widget into MVC, it seems like a gallery module more so than a core module. As a core module, it should be there because there is at least one driving use case.

You're suggesting TreeView is that use case (are there others?), but given I am taking a different approach for DT and @derek is exploring a markup factory approach for Button, I suspect we need to come together around a single (or fewer than three) solution, or 3.5.0 will represent a loss of cohesion to our story for how to build Widgets.

I'm willing to accept (though not happy about) the idea that DT and TreeView (and Button if needs be) will have separate stop gap solutions in 3.5.0 for quasi-MVC implementations until the real thing comes along in 3.6.0. But if it's avoidable, that would be preferable. I wonder which approach closer approximates your vision of the future.

@natecavanaugh
Copy link

Hey guys,
I don't know how far you got with this (either the WidgetStringRenderer, or the MVC breakup of Widget), but in @sdesai's description of the new Widget (Base+Lifecycle+View+Model+Controller), how would the string renderer fit in there?

Would it be a plugin, an extension of the view that you compose in, or something else altogether?

We've run into the need for this in multiple components (trees, toolbar w/buttons, our textboxlist, and also other components custom to Liferay).
The other use case I can see, besides tree, would be building an entire UI from the JS. ExtJS recently did a kind of similar change in that they modified their rendering engine to do bulk updates so that each child component can contribute it's rendered state to it's owner (or parent) component, then at the end do one big render.

Basically, all components can have an owner, and the children all sit inside of the owner's (boudning|content)Box, and during some part of the lifecycle (probably render), the owner gathers up the children DOM (string or Nodes)*, and if it has an owner, pass it up, if it doesn't, flush the buffer of HTML into the render location.

I wonder for the P.E. scenario, I wonder if the same idea could be leveraged, where it contributes HTML_PARSER attributes, and right after render this is resolved.

*Resolving the contribution of either strings or nodes might be a bit hairy, though I can think of some ways to handle that, for instance, using stamped placeholder divs that are immediately swapped out with the component nodes on render)

Of course, you guys may have already solved all of this, and if so, ignore these as the ramblings of someone 6months behind :)

@sdesai
Copy link
Author

sdesai commented May 17, 2012

Hey Nate,

The breakup of Widget into these separate pieces doesn't appear to be on the near term horizon, as far as the base Widget goes. It's mainly because I haven't had a chance to think through how we'd get there, while maintaining backwards compatibility. But I'd imagine if it was broken out it would be:

StringRendering Based Widget : Widget = Base + Lifecycle + StringBasedView + Model + Controller 

The basic idea being - I should be able to switch out Views with the same Model, and one of those views could be purely string/template based.

Whether these are extensions or plugins doesn't make that much difference at the high level.

The idea is to be able to re-use/extend/switch out each piece independently.

That said, at the lower level, "extend" is where it does make a difference - since we'd need full "multiple inheritance" support, if you wanted to mix in an extension with it's own prototype chain.

If they are "plugins", they'd be separate objects which interact with each other. The downside there being the potential performance overhead of a single object now being three separate objects - the M, V and C instances. Not a big deal for a handful of instances, but for Tree etc, it may bubble up. If they were "extensions", they'd go through Y.Base.create, to create a single object (with the multiple inheritance drawback).

If I could start from scratch on a Widget2 base class, it would be broken out, with separately extendable M, V and C pieces. That doesn't mean folks extending Widget, can't begin to explore breaking out the M, V and C responsibilities which currently get rolled into a single class, as Luke is doing for the refactored DataTable.


As for the WidgetStringRenderer, I'd be interested in your feedback on this initial approach, which we're trying to iron out the gaps for:

https://github.com/sdesai/yui3/blob/widget-htmlrenderer/src/widget/tests/manual/widget-htmlrenderer.html
https://github.com/sdesai/yui3/blob/widget-htmlrenderer/src/widget/tests/manual/tree-htmlrenderer.html

Which is essentially the implementation based on the discussion above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment