Comparing Widget in Y2 Versus Y3

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.