jQuery Widget factory vs. my Controller factory

Posted by stephen on June 15th, 2011

I’ve been working on a stateful/event driven model for jQuery “widgets” for a while now. For the sake of differentiating theirs and mine in this article, I’ll call mine the “controller” factory. I read the jQuery widget factory post a while back and basically dismissed/forgot about it at the time — not sure why. I stumbled onto it again today and wished I’d become accustomed to using it before creating my own, but I have some thoughts on it. Mine is more complicated without much added benefit, so I really like the simplicity of the widget factory.

My main complaint is probably pretty common, but since it’s not voiced on the post, I’ll do it here. There are no comments on their article, which makes me suspect the author is not moderating comments, and posting it there would be no good. Anyways, onto the meat…

General organization and instantiation

There should be support for multiple namespaces, and isolation of the widgets. When developing a website for multiple countries and locales with a large set of encapsulated and overlapping functional requirements, the hierarchy gets pretty complex, and without proper namespacing, my opinion is that it gets plain ugly. Simple sites can have simple namespaces, so namespacing should not make something more complex than it needs to be. For my controller factory, I have something like:

$("selector").controller(namespace.subns.ClassName, options);

Another difference is that I return the class instance so that it can be interacted with in the standard js manner, i.e.:

$("selector").controller(namespace.subns.Classname).controllerMethod(1,2,3);

I’m not sure which I like more, but I do like that the widget factory keeps the chainability of the jQuery methods. In my controller factory, I’ve proxied all of the jQuery methods on the base controller class to allow for this, but there’s merging and conflict possibilities with the controller prototype that I don’t like.

Events

I love that you can allow callbacks to be passed the widget factory as a property of the options parameter, then use their _trigger method to magically call the callback function and dispatch an event with that name. I think the benefit of a callback function is that it’s the first “handler” called after the method, so the caller of a method can have dibs on the initial response. The only deficit of a callback I can think of is that it’s not actually part of the event chain, which is petty.

My controller factory provides a “createDispatcher” method that creates a proxy function which dispatches an event. In other words, in the constructor of my controller I’ll create an “init” dispatcher with:

this.createDispatcher({init:"_init"});

That will result in a this.init() method that dispatches an “init” event (with the class name as the event namespace), and the “_init” method is bound as the first handler for that event. This way, even the method of the current instance is part of the event chain.

In addition, you can pass events to dispatch before and after the primary dispatcher.

this.createDispatcher({init:"_init", beforeInit:"_beforeInit", afterInit:"_afterInit"});

If the result of beforeInit is false, init will not be called (sort of like a bubble); otherwise, the beforeInit event object is attached to the init event object for reference. The same applies for the init and afterInit events (if one is false, the next doesn’t fire).

A huge benefit of the event proxy method is that extension classes can simply override the _init, _beforeInit, etc methods and still apply the “super” class’s method without worrying about it dispatching the event. In other words, say you have a Person class and a Child class.

Child.prototype._init = function(){
Person._init.apply(this, arguments);
};

That will work. If you’re dispatching events from directly within the method, as with the widget factory, trying to apply a super class’s method will dispatch god knows what events. Then, if you can’t work with that, you’ll have to copy all of the logic to your overriding method and dispatch the event where you want, and that’s a waste of reusable code.

jQuery.fn.anchorPost - post with a standard anchor tag

Posted by stephen on December 10th, 2010

It’s annoying to build a whole form when you just want a link to post to a page, and sometimes you have to worry about that link already being in a form. I created this little plugin to dynamically build a form at the bottom of the page and submit it.

The action/target attributes of the form use the anchor tag’s href/target attributes. The method of the form is always post. By default, the plugin looks for a data-post attribute with a serialized string of values to post. For example, your link would look like this:

<a href="form-page/post.php" target="_blank" data-post="action=delete&id=1">Delete Record</a>

The js you’d use is $(”a[data-post]“).anchorPost();

If you’re using something other than the data-post attribute, you can pass a function that is applied to the anchor element, like so:

$("a.selector").anchorPost(function(){ return $(this).find("span.id").text(); });

This uses my $.unserialize plugin. And here’s the plugin!

/* Converts a standard link to a form POST submission */
$.fn.anchorPost = function(postFn){
//Define default postFn function.
if(postFn==null){
postFn = function(){
return $(this).attr("data-post");
}
}
//Loop through elements in the jQuawesome
this.each(function(a, anchor){
var $a = $(anchor),
form = $("<form id='entries_form' method='post' />").appendTo("body").hide(),
formVars = postFn.call(anchor);
//Copy target and href attributes from subject anchor.
form.attr("target", $a.attr("target"));
form.attr("action", $a.attr("href"));
//Create hidden inputs for the form vars.
if(!$.isPlainObject(formVars)){
formVars = $.unserialize(formVars);
}
for(var i in formVars){
form.append("
“);
}
//Remove href and target from anchor so it doesn’t do anything, which allows it to be used for something else, if the form is being opened in a diff window.
$a.attr(”href”,”javascript:;”);
$a.removeAttr(”target”);
//Submit the form when the anchor is clicked.
$a.click(function(){
form.submit();
});
});
//Return me for for method chaining
return this;
};

jQuery.unserialize

Posted by stephen on December 10th, 2010

Maybe it’s just all the PHP I’ve been digging around lately, but I could’ve sworn jQuery had a method for unserializing a string.

[code]/* Converts a serialized string into a simple object */
$.unserialize = function(str){
var chunks = str.split(”&”),
obj = {};
for(var c=0; c var spl = chunks[c].split("=", 2);
obj[spl[0]] = spl[1];
}
return obj;
};[/code]

I needed this for my $.fn.anchorPost plugin.

jQuery templates

Posted by stephen on December 10th, 2010

I’ve written a number of my own methods for templatizing a block of HTML to be used in “repeaters” with variable placeholder and such. I was just browsing api.jquery.com and noticed they have something like this in beta. http://api.jquery.com/category/plugins/templates/

Very exciting! Looking foward to making use of it and seeing it in its final release stage…

Javascript Random Between -1 and 1 (inclusive)

Posted by stephen on September 9th, 2010

This is one of those things that took me way longer than it should have, so hopefully writing it here will save us some time later on.

I was simply trying to randomize an array sort, and all of the examples I found showed something like this:

[1,2,3,4].sort(function(a, b){ return Math.random()-0.5; };

Math.random() RAAARELY returns 0, so part of the randomization is left out, sort of, because the function never considers two elements the same.

I wanted to return -1, 0, or 1, and was determined that it shouldn’t be difficult to generate one of those randomly. I ended up with this, which works perfectly.

[1,2,3,4,5].sort(function(a, b){ return Math.round(Math.random() - Math.round(Math.random())); };

Basically, we replace “- 0.5″ with “- Math.round(Math.random())” and also round our entire result.

Using TinyMCE content_css, body_class, etc settings in Magento

Posted by stephen on July 15th, 2010

I needed to add a stylesheet and body class to the Magento TinyMCE editor, and had some trouble figuring out why my settings weren’t working. Here’s what I ended up doing…

First, rename /js/tiny_mce/themes/advanced/editor_template_src.js to editor_template.js (delete or rename the one that’s already there to something like editor_template.js.bak). Look for this block in the init method around line 65:

// Default settings
t.settings = s = extend({
theme_advanced_path : true,
theme_advanced_toolbar_location : 'bottom',
theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect",
theme_advanced_buttons2 : "bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code",
theme_advanced_buttons3 : "hr,removeformat,visualaid,|,sub,sup,|,charmap",
theme_advanced_blockformats : "p,address,pre,h1,h2,h3,h4,h5,h6",
theme_advanced_toolbar_align : "center",
theme_advanced_fonts : "Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats",
theme_advanced_more_colors : 1,
theme_advanced_row_height : 23,
theme_advanced_resize_horizontal : 1,
theme_advanced_resizing_use_cookie : 1,
theme_advanced_font_sizes : "1,2,3,4,5,6,7",
readonly : ed.settings.readonly
}, ed.settings);

For general class selectors that you want to show in the dropdown, you can use the “theme_advanced_style_formats” setting.

theme_advanced_style_formats : "alert-text, happy-text",

I proceeded to add body_class and content_css settings to this object, then dug around for an hour or two trying to find why they weren’t being applied. There’s a hierarchy at work here that I’m sure would be apparent if I were more familiar with TinyMCE’s inner workings.From the looks of it, maybe it’s only properties that start with “theme_” that should be added here (readonly is probably a generic prop for all/many settings object). Anyways, I basically found that we have to edit the editor’s settings directly, rather than the theme’s. So, above t.settings = s = …, I added this:

extend(ed.settings, {
content_css:"/skin/frontend/enterprise/gf/css/content.css",
body_class:"main"
});

And there you have it!

Banner Widget sequence is out of order

Posted by stephen on June 29th, 2010

I noticed that when displaying the Enterprise Banner Widget in “fixed” mode, the order is ignored. I thought it might have something to do with my template, but the template that ships with the Widget (/app/design/frontend/enterprise/template/banner/widget/block.phtml) does the same thing.

I tracked the order issue down to Enterprise_Banner_Block_Widget_Banner::getBannerIds() in /app/code/core/Enterprise/Banner/Block/Widget/Banner.php, line 137.

$bannerIds = $this->_bannerResource->getExistingBannerIdsBySpecifiedIds($bannerIds);

That’s Enterprise_Banner_Model_Mysql4_Banner::getExistingBannerIdsBySpecifiedIds() in /app/code/core/Enterprise/Banner/Model/Mysql4/Banner.php. It’s making a call to the database to see which of the banners are enabled, and then returning the id column, sorted, so the items always end up ordered by the banner id, rather than the specified order.

To fix this, I copied /app/code/core/Enterprise/Banner/Model/Mysql4/Banner.php to /app/code/local/Enterprise/Banner/Model/Mysql4/Banner.php and changed getExistingBannerIdsBySpecifiedIds() to the following:

public function getExistingBannerIdsBySpecifiedIds($bannerIds, $isActive = true)
{
$adapter = $this->_getReadAdapter();
$select = $adapter->select()
->from($this->getMainTable(), array('banner_id'))
->where('banner_id IN (?)', $bannerIds);
if ($isActive) {
$select->where('is_enabled = ?', (int)$isActive);
}
$existingIds = $adapter->fetchCol($select);
foreach($bannerIds as $key=>$bannerId){
if(array_search($bannerId, $existingIds) === false){
unset($bannerIds[$key]);
}
}
return array_values($bannerIds);
}

The only changes are from $existingIds = … return array_values($bannerIds).

Instead of returning the column straight from the database, we loop through the $bannerIds that were passed to the function, unset the keys that don’t exist in the database col, then return a 0-N indexed array with array_values().

jQuery.animate “too much recursion” from complete callback

Posted by stephen on June 11th, 2010

I was working on a class for sequencing a set of DOM elements, and ran into a snag using the animate function like this:

$.fn.animate.apply($item, this.transitionOutArgs);

My animate args (properties, options) looked something like this:

[{opacity:.1}, {duration:1500, complete: function(){ $(this).css("display","none"); }}]

Firebug kept saying “too much recursion”, and only after the second time the code was run. After a bit of head scratching and some digging into the jQuery code, I saw that it copies the options.complete callback and replaces it with a function that calls the old callback. So, after passing in my complete callback the first time, this.transitionOutArgs[1].complete() was reassigned to the proxy-ish function, and each time after, the proxy-ish function was calling itself.

It’d be nice if jQuery created another callback, such as options.$complete(), that proxies for our explicitly defined options.complete callback, instead of reassigning it, but they may have their reasons. To work around this, I just copied the args before passing them to the function.

$.fn.animate.apply($item, $.extend(true, [], this.transitionOutArgs));

If you’re declaring the transitionOutArgs in the same scope as where the animate function is called, there is no need to copy it, since it is not being referenced and reused. That is the typical use case.

Custom Events in JavaScript

Posted by stephen on December 29th, 2009

Usually I use jQuery for custom events, but recently I’ve been working on a library that I want to be independent of other libraries, so I needed to create something to handle events on custom classes/objects.

For this sample, I changed the namespace to “ns”. You can change that to whatever you’d like.

Eventify

To “eventify” an object, you’ll use the ns.events.eventify(obj) method. Once that is done, your object will have “events” and “__listeners” properties, in addition to “dispatchEvent(eventType, eventData)”, “addEventListener(eventType, callback, bubbles)”, and “removeEventListener(eventType, callback||listener)” methods.

You may pass one or many objects to the eventify() method.
ns.events.eventify(obj);
or
ns.events.eventify(obj1, obj2, obj3);

You do not need to interact with the “events” and “__listeners” properties at all, unless you want to predefine a set of events for a particular object, usually for documentation purposes. Here’s an example of a custom class that uses this event class and predefines its events. Just to clarify, I am using the “id” property in this class for the event bubbling example that follows. It is not needed at all for the events to work.

addEventListener() / dispatchEvent()


ns.MyClass = ({
function MyClass(id){
ns.events.eventify(MyClass.prototype);
this.id=id;
}
MyClass.prototype={
id:null,
events:{
ready:new ns.events.Event("ready"),
error:new ns.events.Event("error")
},
property:null,
method:function(){}
}
return MyClass;
})();

If you do not define the events explicitly in the events property, they will be added automatically when a listener is added to the object. For instance:

var my = new ns.MyClass("my");
my.addEventListener("ready", function(){
alert(this.id+" is ready");
});

Would define my.events.ready.

dispatchEvent() and passing data

To dispatch an event, use the dispatchEvent() method. To pass data with a dispatched event, use the eventData parameter. For example:

my.addEventListener("ready", function(evt, data){
alert(data.hello);
});
my.dispatchEvent("ready", {hello:"Hello from dispatcher."});

Event bubbling

Event bubbling is manually assigned in an event’s “bubbleTo” property, because these are custom objects with no inherent heirarchy. The object defined as the “bubbleTo” property must share the event type that is being dispatched on the current object (they do not need to be instances of the same class). Implement bubbling as follows:

var parent = new ns.MyClass("parent");
var child = new ns.MyClass("child");
child.events.ready.bubbleTo = parent;

child.addEventListener(”ready”, function(evt){
alert(”sincerely, “+evt.target.id+”.”);
});
parent.addEventListener(”ready”, function(evt){
alert(”Sincerely, “+evt.target.id+”. With regards, “+evt.currentTarget.id+”.”);
});

child.dispatchEvent(”ready”);

To prevent subsequent callbacks on an object, call evt.stopImmediatePropagation(). To stop the bubbling, call evt.stopPropagation().

You may also prevent the bubbling of an event when creating the listener by using the “bubbles” parameter of the “addEventListener()” method.

var listener = child.addEventListener("ready", function(evt){
alert("sincerely, "+evt.target.id+".");
}, false);

removeEventListener()

Finally, to remove a listener from an object, use the removeEventListener(listener||callback) method. This method will accept the listener or the callback function used. Note that in the above example I have assigned the listener to “var listener”.
child.removeEventListener("ready", listener);
You may also use the remove() method of the listener itself.
listener.remove();

Download

Right click and choose “Save Target As” to download events.js.

Javascript Date difference as milliseconds, minutes, hours, days, weeks, months, or years

Posted by stephen on November 13th, 2009

I needed to figure out the difference between two dates in Javascript, so I wrote up this function. It takes into account the days in each month as well as leap years. It will calculate years, months, weeks, days, hours, minutes, seconds, and milliseconds, or any combination of those. For example, if you want months and hours, this’ll do ya.

Let me know if anyone finds a discrepancy in the calculations.

sample.html
timeSpan.js

View source of the sample page above for usage. Samples include a date comparison and a date countown.


Copyright © 2007 Stephen Rushing. All rights reserved.