Enyo Daily #5 - Lists, Repeaters, and Flyweights - Part 2
Part 1 of this topic covered Flyweights and how they work. Flyweights themselves are a pretty low level control and not an ideal control with which to develop. There are a couple other controls one step up the abstraction hierarchy for rendering "lists" of items: VirtualRepeater and Repeater.[[MORE]]
In my current project, I needed just such a control. Specifically, here's the problem I needed to solve:
Render a configurable number of instances of a control laid out in a horizontal row.
I tried 3 different approaches:
- Programmatic loop using createComponents()
- enyo.VirtualRepeater
- enyo.Repeater
The first iteration used a simple for loop to create all the components in create() using createComponents(). That was functional but the resulting code was a little tough to manage (mainly due to some unique requirements for some instances of the loop).
Next, I tried using VirtualRepeater. This was a little better because I only had 1 set of Controls with which to interact but also introduced a new issue: rendering the items horizontally rather than vertically. There are actually 2 issues here. First, VirtualRepeater doesn't appear to support layoutKind. Second, the components defined as children of the VirtualRepeater are not, in fact, childNodes in the DOM. Instead, there's an intermediate <div> that groups elements -- the number of which is determined by the value of stripSize. I was able to work around this using CSS:
.extras-hflexrepeater, .extras-hflexrepeater > * {
display:-webkit-box;
-webkit-box-orientation:horizontal;
-webkit-box-pack:start;
-webkit-box-align:stretch;
}
The final solution -- and the one I elected to use -- is the Repeater. The key design difference between the VirtualRepeater and the Repeater is that whereas the VirtualRepeater uses the Flyweight to manage the DOM nodes, the Repeater simply adds more enyo Controls which each manage their own DOM nodes. In this way, the Repeater is more akin to the programmatic loop. In fact, that is precisely how it is implemented under the covers.
The choice between the two repeater controls is more about preference in my opinion. From a development perspective, you have to manage indices in one way or another -- either for the "row" or for the component id. You gain some efficiency through the auto-selection of rows on event with the Flyweight but that can be mitigated equally well using custom properties on Controls when using the Repeater.
I like to wrap these posts up with a functional code example. In this case, I've included both a VirtualRepeater and Repeater with the same results to illustrate how they're both used. Note that the VirtualRepeater code requires the CSS from above.
var _Example = {
name:"com.technisode.example.App",
kind:"Control",
components:[
{kind:"Scroller", height:"80px", vertical:false, components:[
{kind:"VirtualRepeater", name:"fw", className:"extras-hflexrepeater", onSetupRow:"setupVirtualRepeaterRow", components:[
{width:"120px", components:[
{kind:"Control", name:"text"},
{kind:"Button", name:"button", onclick:"buttonClicked"}
]}
]}
]},
{kind:"Scroller", height:"80px", vertical:false, components:[
{kind:"Repeater", name:"repeater", layoutKind:"HFlexLayout", onSetupRow:"setupRepeaterRow"}
]}
],
buttonClicked:function() {
// the node is automatically mapped to the right instance on event
// but the enyo-managed data (e.g. this.$.text.getContent()) isn't.
// see enyo.StateManger for a mechanism to save/restore component state
console.log("clicked",this.$.text.node.innerText);
},
setupVirtualRepeaterRow:function(source, index) {
if(index<25) {
this.$.text.setContent("I'm instance "+index);
this.$.button.setCaption("Button #"+index);
return true;
}
},
setupRepeaterRow:function(source, index) {
if(index<25) {
// note that names have to be unique because, unlike VirtualRepeater,
// each of these enyo.Controls will exist as children of Repeater
return {width:"120px", components:[
{kind:"Control", content:"I'm instance "+index, name:"text"+index},
{kind:"Button", name:"button"+index, caption:"Button #"+index, onclick:"buttonClicked"}
]}
}
}
};
enyo.kind(_Example);