Enyo Daily #2 - Lists, Repeaters, and Flyweights - Part 1
The new approach for list components in enyo has proven to be a difficult topic to fully understand. I don’t pretend to have a complete grasp but rummaging through the source code has helped me immensely. Today, I want to cover the magic that is the Flyweight.
[[MORE]]The Flyweight is appropriately described by the enyo API docs as “a control designed to be rendered multiple times.” It stands to reason why this kind of a control would be good for lists. The Flyweight enables you to define a template for a list item and the list renders the Flyweight multiple times – once for each list item. If you’ve used the VirtualList or VirtualRepeater controls, you’ll know that you don’t have to define a Flyweight to get things running; those controls hide the Flyweight. Nonetheless, it’s handy to understand how the Flyweight works because its design directly affects the design of the higher level controls.
The most important feature of Flyweights is that unlike most enyo controls where the control has a single top-level node, it may have multiple nodes for a single enyo control. This is where things begin to get murky for devs. Consider the following code using the VirtualRepeater. VirtualRepeater, as the name inplies, will create a node containing the controls declared in its component block for each successful return from onSetupRow. It accomplishes this by employing the Flyweight.
So, if in setupRow() I configure the repeater to have 10 rows, how would I later modify the text of the fifth row, for example?
{
kind:”VirtualRepeater”,
name:”myRepeater”,
components:[
{name:”text”},
{name:”button”,kind:”Button”, onclick:”buttonClicked”}
],
onSetupRow:”setupRow”
}
As always, there are several ways to solve the problem but the one I’ll use relates to setting the “focus” of the Flyweight. Because the Flyweight has a single component that represents multiple instances in the DOM, you have to tell it on which instance you wish to operate. VirtualRepeater exposes a method to do such a thing: controlsToRow(inRowIndex). Under the covers, this method calls setNode() on the Flyweight to “focus” it on the right instance. After that, you can reference the components as you would any other in enyo.
highlightRow:function(rowIndex) {
this.$.myRepeater.controlsToRow(rowIndex);
this.$.text.setClassName(“highlighted-row”); // invented class for illustration
}
The Flyweight has a nice feature that will focus on an instance in response to an event that occurs on control of that instance. Using our example above, when buttonClicked is called, the instance containing the button will automatically have focus so no additional calls are necessary:
buttonClicked:function(source, event) {
// no need to call controlsToRow beforehand
this.$.text.setContent(“I’ve been clicked!”);
}
There is a little extra going on in VirtualList and VirtualRepeater to make this work correctly. While Flyweight will automatically link itself to an instance's node on event and you can explicitly focus an instance using setNode(), this only ties the Flyweight to the node, not the enyo Component. The List and Repeater components employ a StateManager component to save and restore the state of components when the Flyweight's focus changes. Take a look in the source of those components as well as the RowServer for detail
To wrap up, here’s a complete example using Flyweight directly to illustrate:
var _Example = {
name:"com.technisode.example.App",
kind:"Control",
components:[
{kind:"Flyweight", name:"fw", layoutKind:"HFlexLayout", components:[
{kind:"Control", name:"text"},
{kind:"Button", name:"button", onclick:"buttonClicked"}
]}
],
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);
},
getInnerHtml:function() {
var h = [];
// make a bunch of instances of the flyweight
for(var i=0;i<10;i++) {
this.$.text.setContent("I'm instance "+i);
this.$.button.setCaption("Button #"+i)
h.push(this.getChildContent());
}
// see comment in enyo.RowServer.generateRow()
this.$.fw.needsNode = true;
return h.join("");
}
};
enyo.kind(_Example);