I have been wanting to do a YUI 2 versus YUI 3 comparison for some time, and it took a while to design a simple example that was complex enough to be meaningful. Anyway, for this comparison I wrote a simple CheckboxList widget, that renders a list of checkboxes and labels from a JSON object. Both versions will require only a DOM node to instantiate, they will include two custom events onCheck and onBeforeCheck, and they will have these public functions: 'clear', 'hide', 'render', 'serialize', 'show'. The 'hide' and 'show' methods will apply "display:none" and "display:block" to the root node, respectively. The 'clear' method will remove the content of the root node and then call 'hide'. The 'render' method will build the HTML from a JSON object and then call 'show'. Lastly, the 'serialize' method returns the values of the checkbox as an AJAX ready query string.
Example 1: YUI 2 CheckboxList
(function()
{
var Y = YAHOO,
YL = Y.lang,
YU = Y.util,
YD = YU.Dom,
YE = YU.Event,
ITEM_TMPL = '<li><input id="{id}" name="{name}" type="checkbox" value={value} {checked}/><label for="{id}">{label}</label></li>',
_evtOnClick = function(e)
{
var targ = YE.getTarget(e);
if ('input' === YD.getTagName(targ))
{
if (this.fireEvent(_F.CE_BEFORE_ONCHECKED, e))
{
this.fireEvent(_F.CE_ONCHECKED, e);
}
else
{
YE.stopEvent(e);
}
}
},
_F = function(elem, conf)
{
var _this = this;
_this._cfg = YL.isObject(conf) ? conf : {};
if (! YL.isString(_this._cfg.maxHeight)) {_this._cfg.maxHeight = _F.ATTR.maxHeight;}
_this._node = YD.get(elem);
_this._tmpl = ITEM_TMPL.replace(/\{name\}/g, _this._cfg.name || _F.ATTR.name);
_this.createEvent(_F.CE_BEFORE_ONCHECKED, _this);
_this.createEvent(_F.CE_ONCHECKED, _this);
YE.on(_this._node, 'click', _evtOnClick, _this, true);
};
_F.ATTR =
{
maxHeight: '100px',
name: 'checkboxListValue[]'
};
YL.augmentObject(_F,
{
CE_BEFORE_ONCHECKED: 'before_onchecked',
CE_ONCHECKED: 'onchecked'
});
_F.prototype =
{
_cfg: null,
_node: null,
_tmpl: null,
_renderItem: function(id, label, value, isChecked)
{
return this._tmpl.replace(/\{id\}/g, id).replace(/\{label\}/g, label).replace(/\{value\}/g, value).replace('{checked}', isChecked ? 'checked="checked"' : '');
},
clear: function()
{
this.hide();
this._node.innerHTML = '';
},
hide: function()
{
YD.setStyle(this._node, 'display', 'none');
},
render: function(json)
{
var i = 0, j = json.length, o, sb = ['<ul>'];
for (; i < j; i += 1) {
o = json[i];
sb[i + 1] = this._renderItem(o.id, o.label, o.value, o.isChecked);
}
sb[i + 1] = '</ul>';
this._node.innerHTML = sb.join('');
if (this._cfg.maxHeight.replace(/\[\d\.]+/, '') < YD.getStyle(this._node, 'height').replace(/\[\d\.]+/, ''))
{
YD.setStyle(this._node, 'height', this._cfg.maxHeight);
}
this.show();
},
serialize: function()
{
var sb = [],
npts = this._node.getElementsByTagName('input');
for (var i = 0, j = npts.length, npt; i < j; i += 1)
{
npt = npts[i];
if (npt.checked)
{
sb.push(npt.name + '=' + npt.value);
}
};
return sb.join('&');
},
show: function()
{
YD.setStyle(this._node, 'display', 'block');
}
};
YL.augment(_F, YU.EventProvider);
Core.Widget.CheckboxList = _F;
}());
To render the checkboxes we use a template string 'ITEM_TMPL' and regular expressions to replace the content from a JSON object. The JSON object should be an array of objects with four keys: 'id', 'label', 'value', 'isChecked'. There is one click event attached to the root node that will fire the custom events as necessary. There are two other properties you can adjust, the 'maxHeight' of the root node, which will cause scrollbars to appear when you have "overflow:scroll" applied via css, and the 'name' to give each input. In the YUI 2 example we are also augmenting the class with EventProvider to simplify event subscription and firing. Also, the state of the widget is stored on the '_cfg' object.
Example 2: Y3 Checkbox Widget
YUI().add('checkboxList', function(Y)
{
var Lang = Y.Lang,
ITEM_TMPL = '<li><input id="{id}" name="{name}" type="checkbox" value={value} {checked}/><label for="{id}">{label}</label></li>',
_F = function(conf)
{
_F.superclass.constructor.apply(this, arguments);
};
_F.ATTRS =
{
// the json for rendering
json:
{
lazyAdd: false,
setter: function(v)
{
if (! Lang.isArray(v))
{
Y.fail('CheckboxList: Invalid json provided: ' + typeof v);
}
return v;
},
value: []
},
// the maximum height to make the list
maxHeight:
{
value: '100px'
},
// name to apply to each checkbox
name:
{
value: 'checkboxListValue[]'
},
// The root node for this widget.
node:
{
setter: function(node)
{
var n = Y.get(node);
if (!n)
{
Y.fail('CheckboxList: Invalid node provided: ' + node);
}
return n;
}
},
// the template item
templateItem:
{
value: ''
}
};
_F.NAME = "checkboxList";
_F.CE_BEFORE_ONCHECKED = 'before_onchecked';
_F.CE_ONCHECKED = 'onchecked';
Y.extend(_F, Y.Widget,
{
_evtOnClick: function(e)
{
var targ = e.target;
if ('input' === targ.get('tagName').toLowerCase())
{
/*
not working the same in YUI 3 as in YUI 2
if (this.fire(_F.CE_BEFORE_ONCHECKED, e))
{
e.halt();
return;
}
*/
this.fire(_F.CE_ONCHECKED, e);
}
},
_renderItem: function(id, label, value, isChecked)
{
return this.get('templateItem').replace(/\{id\}/g, id).replace(/\{label\}/g, label).replace(/\{value\}/g, value)
.replace('{checked}', isChecked ? 'checked="checked"' : '');
},
clear: function()
{
this.hide();
this.get('node').set('innerHTML', '');
},
destructor: function()
{
this.clear();
this._nodeClickHandle.detach();
},
hide: function()
{
this.get('node').setStyle('display', 'none');
},
initializer: function(config)
{
this.set('templateItem', ITEM_TMPL.replace(/\{name\}/g, this.get('name')));
},
bindUI: function()
{
this._nodeClickHandle = this.get('node').on("click", Y.bind(this._evtOnClick, this));
},
renderUI: function()
{
var json = this.get('json'),
i = 0, o,
j = json.length,
sb = ['<ul>'],
node = this.get('node');
for (; i < j; i += 1)
{
o = json[i];
sb[i + 1] = this._renderItem(o.id, o.label, o.value, o.isChecked);
}
sb[i + 1] = '</ul>';
node.set('innerHTML', sb.join(''));
if (this.get('maxHeight').replace(/\[\d\.]+/, '') < node.getStyle('height').replace(/\[\d\.]+/, ''))
{
node.setStyle('height', this.get('maxHeight'));
}
},
serialize: function()
{
var sb = [],
npts = this.get('node').getElementsByTagName('input');
npts.each(function(npt, i)
{
if (npt.get('checked'))
{
sb.push(npt.get('name') + '=' + npt.get('value'));
}
});
return sb.join('&');
},
show: function()
{
this.get('node').setStyle('display', 'block');
}
});
Y.CheckboxList = _F;
}, '1.0.0' ,{requires:['widget'], use: []});
Besides the functions used in the YUI 2 example, the YUI 3 example has several others functions that are part of the widget class 'lifecycle', which we are extending. The 'lifecycle' concept, introduced in YUI 3, is where certain functions are called automatically by the framework ('destructor', 'initializer', 'bindUI', 'renderUI'). The 'initializer' is called when the class is instantiated and the 'destructor' when the instantiated object is destroyed. So in the YUI 3 example, the setup of the 'ITEM_TMPL' constant, has been moved into the 'initializer', instead of in the constructor function. Additionally, YUI 3 automatically manages your class variables for you, as long as you attach the 'ATTRS' object to the constructor function (see Attribute for all the options). Lastly, the Widget class adds a 'render' function that automatically calls 'renderUI', 'bindUI', and a third function 'syncUI' that I am not using, so I have not explicitly written a render function.
Note: One thing that really confused me about 'render' is that the YUI documentation talks about it as being part of the 'lifecycle'. I assumed that this meant 'render' would be called when 'initializer' was called, but that is not the case. It just means that when you call 'render' that 'renderUI', 'bindUI', and 'syncUI' are called.
The rest of the differences are mostly semantic, instead using pseudo-protected variables to store values as we do in YUI 2, we use the 'get'/'set' methods that are automatically provided, which write to and from the attributes defined from the 'ATTRS' object. Additionally, DOM and Event manipulation is called directly from the YAHOO object in YUI 3, instead of the static objects as was done in YUI 2.
I have put both examples together on a Test Page.
