Refactoring Code in to an Object-Oriented Paradigm #6: Compatibility

This is article number 6 of a series of article, which simply documents my refactoring of some code I originally wrote (pretty poorly) last year. To get the gist of what’s going on. Check the original post – Refactoring Code in to an Object-Oriented Paradigm.

Making Things Work for Everyone

We’re nearly finished with our refactor, we’ve added some great functionality, made things a lot cleaner and more efficient…that is for the browsers that can run our code. The problem is that, while cross-browser compatibility has got a lot easier, it’s still an issue. It’s only made worse by the legacy versions of IE which are still clinging on, and until the majority of IE users are on IE10, we’re still going to see some large differences or deficiencies for those users.

Because our code is quite simple, there aren’t a huge number of compatibility issues, but there are a few. These issues are also quite common when using Javascript. So we’ll sort them out, explaining whats and whys of each solution.

Event Handlers

I mentioned in the last article how I’d “continue side-stepping the compatibility issues which exist” regarding native event handling. The issue with native event handling is that there are two flavours that are definitely required and another that should be used to be safe. That doesn’t make for nice, quick code. But I wrote the code in the last article with extensibility in mind, so we can simply add compatibility for other browsers without affecting the rest of the code. Here’s our original code…

/** Set a native event listener (eventually regardless of the browser you're using).
*
*   @param eventElm The element to which the element to listen for, will involve.
*   @param eventType The type of event to listen for.
*   @param callback The function to call when the event happens.
*   @return True if listener was set successfully, false otherwise.
*/
AutoScrobbler.prototype.setNativeListener(eventElm, eventType, callback) {

eventElm.addEventListener(eventType, callback, false);

}

Conveniently, the MDN page for this has a bare-minimum shim documented, and details older methods of event handling as well. So we’ll use this as a guide on how to add compatibility to this function.

/** Remove a native event listener regardless of the browser you're using.
*
*   @param eventElm The element that had been listening.
*   @param eventType The type of event that was being listened for.
*   @param callback The function that was set to trigger.
*   @return Undefined if successful, otherwise an exception will be thrown.
*/
AutoScrobbler.prototype.setNativeListener(eventElm, eventType, callback) {

if (eventElm.addEventListener)

eventElm.addEventListener(eventType, callback, false);

else if (eventElm.attachEvent)

eventElm.attachEvent('on' + eventType, callback);

else {

eventElm['on' + eventType] = callback;
if (typeof eventElm['on' + eventType] == "undefined")

throw "EventHandler Error: The event could not be set";

}

}

This version includes far more browser support, and goes a step further in that it will even support the very old version of event handlers. However, it does currently assume that every single recognised event by addEventListener is simply an unprefixed version of the events recognised by both attachEvent and the older element.event style handler. This is certainly not true in some cases, for instance the “textInput” event isn’t at all support by attachEvent, the closest you can get to this event is “onpropertychange”. If we wanted this level of support, we would need to have an exceptions list, which would change these values based on what event handler was to be used. However, it works for simple ‘click’ events and ‘mouseover’ events. We can also add a complementing removeNativeListener function too.

/** Set a native event listener regardless of the browser you're using.
*
*   @param eventElm The element to which the element to listen for, will involve.
*   @param eventType The type of event to listen for.
*   @param callback The function to call when the event happens.
*   @return Undefined if successful, otherwise an exception will be thrown.
*/
AutoScrobbler.prototype.removeNativeListener(eventElm, eventType, callback) {

if (eventElm.removeEventListener)

eventElm.removeEventListener(eventType, callback, false);

else if (eventElm.detachEvent)

eventElm.detachEvent('on' + eventType, callback);

else

eventElm['on' + eventType] = undefined;
if (typeof eventElm['on' + eventType] != "undefined")

throw "EventHandler Error: The event could not be removed";

}

This should now provide us with the support we need for any event handling in the code.

Custom Event Handling

I originally implemented my own method of custom event handling in my article on making code extensible. I chose to do this due to the plethora of compatibility issues which come with custom event handling using native provisions. The most support for the new method was only fully implemented across browsers when IE9 came along. If we were to use it, it would mean that the code would only be available for use in “modern” browsers (so not IE8 or below). For the basic uses we want it for (namely making our code more extensible and future-proof), we don’t want to lose all that support.

There are libraries and shims out there which will make all event handling a one liner. Large libraries such as jQuery and MooTools were practically made for solving cross-browser issues like this, but you certainly wouldn’t want to use one just for this one piece of functionality, as it adds nearly 100kb to your download unnecessarily! There are very small libraries (known as micro libraries) which do just focus on one element of functionality. For example, Events.js provides a cross-browser events system which would solve the problem. The 140medley library actually solves a few problems* besides event handling, but it’s tiny (we’re talking bytes rather than kilobytes) so it may be worth using it anyway.

However, my code is an addition to someone elses code, through this process we’ve actually added to the code drastically (though this has added further functionality). We’ve even added to the number of file requests made by separating out the styles (we could probably sort this out with even more code). While I originally wanted to keep things lean I’ve expanded the code, adding another library on top of this would prove even more weighty! For this reason I am going to keep my custom event handler set up as it is, (though I’ve just added a remove and an initiate function to make it a full implementation), I will make this available on GitHub when I finish this series of articles so other people can use this basic implementation, and if you’d prefer to use someone elses code instead, Nicholas Zakas has a great implementation too.

querySelector

The event handling issues are definitely the worst parts of the code to deal with where compatibility is concerned. But a much more common compatibility issue is the use of element.querySelector(). This function is brilliant for targeting any element on the page using CSS-style selectors. Without it we are limited to functions such as element.getElementById(), which only allows the targeting of one element at a time. As above, jQuery and MooTools have had this selector functionality built in for years. But again, adding a near 100kb file to your load just for this functionality is overkill, I would instead go back to recommending 140medley* which will sort this functionality out for you in less than a kilobyte.

I won’t be using that library here because again, it would add more to our code, and I’d have to include a copyright license to legally use the code. Because the operations are simple, I’m instead going to fall back to the lowest common denominator. I know that all the browsers that I need compatibility in have functions such as element.getElementById(), element.getElementsByClassName() and element.getElementsByTagName(). So I will simply rewrite my code to use these.

...
AutoScrobbler.prototype.addLatest = function() {

var tracks = document.querySelectorAll(".userSong");
//Can instead be written as
var tracks = document.getElementsByClassName("userSong");

... tracks[1].querySelector("input").value ...;
//Can instead be written as
... tracks[1].getElementsByTagName("input")[0].value ...;

...

... item.querySelector("input").value ...
//Can instead be written as
... item.getElementsByTagName("input")[0].value ...

...
... tracks[0].querySelector("input").value;
//Can instead be written as
... tracks[0].getElementsByTagName("input")[0].value;

...

item.querySelector("input").checked = true;
//Can instead be written as
item.getElementsByTagName("input")[0].checked = true;

...

}

I was using querySelector here “because I could” in most cases, rather than “because it was essential”. I wasn’t targeting anything very specific. Even if I had a couple of selectors in each querySelector, it would’ve easy to simply break these down into two steps. I have made things far more compatible, simply by taking time to rethink my code a little. This is the best type of compatibility refactoring, as it requires no extra code, just rewritten code to get further browser support.

We’ve made a few changes here, so I’ll post the full code below, just for reference.

/** AutoScrobbler is a bookmarklet/plugin which extends the Universal Scrobbler
*   web application, allowing automatic scrobbling of frequently updating track
*   lists such as radio stations.
*
*   This is the constructor, injecting the user controls and starting the first
*   scrobble.
*/
function AutoScrobbler() {

var userControls = "<div id=\"autoScrobbler\" class="auto-scrob-cont">\n"+
"<input id=\"autoScrobblerStart\" class="start" type=\"button\" value=\"Start auto-scrobbling\" /> | <input id=\"autoScrobblerStop\" class="stop" type=\"button\" value=\"Stop auto-scrobbling\" />\n"+
"<p class="status-report"><span id=\"autoScrobblerScrobbleCount\">0</span> tracks scrobbled</p>\n"+
"</div>\n";
this.stylesUrl = "http://www.andrewhbridge.co.uk/bookmarklets/auto-scrobbler.css";
this.injectHTML(userControls, "#mainBody");
this.injectStyles();
this.startElm = document.getElementById("autoScrobblerStart");
this.stopElm = document.getElementById("autoScrobblerStop");
this.loopUID = -1;
this.lastTrackUID = undefined;
this.scrobbled = 0;
this.countReport = document.getElementById("autoScrobblerTracksScrobbled");
this.evtInit(["addLatest", "loadThenAdd", "start", "stop"]);
this.listen("addLatest", this.reportScrobble);
this.setNativeListener(this.startElm, 'click', this.start);
this.setNativeListener(this.stopElm, 'click', this.stop);
this.start();

}

/** Inject the css stylesheet into the <head> of the page.
*/
AutoScrobbler.prototype.injectStyles = function() {

var styles = document.createElement('SCRIPT');
styles.type = 'text/javascript';
styles.src = this.stylesUrl;
document.getElementsByTagName('head')[0].appendChild(styles);

}

/** Hashing function for event listener naming. Similar implementation to
*   Java’s hashCode function. Hash collisions are possible.
*
*   @param toHash The entity to hash (the function will attempt to convert
*                 any variable type to a string before hashing)
*   @return A number up to 11 digits long identifying the entity.
*/
AutoScrobbler.prototype.hasher = function(toHash) {

var hash = 0;
toHash = "" + toHash;
for (var i = 0; i < toHash.length; i++)

hash = ((hash << 5) - hash) + hash.charCodeAt(i);

}

/** Custom event initiator for events in AutoScrobbler.
*
*   @param eventName The name of the event. This may be an array of names.
*/
AutoScrobbler.prototype.evtInit = function(eventName) {

//Initialise the evtLstnrs object and the event register if it doesn't exist.
if (typeof this.evtLstnrs == "undefined")

this.evtLstnrs = {"_EVTLST_reg": {}};

if (typeof eventName == "object") {

for (var i = 0; i < eventName.length; i++) {

var event = eventName[i];
this.evtLstnrs[""+event] = [];

}

} else

this.evtLstnrs[""+eventName] = [];

}

/** Custom event listener for events in AutoScrobbler.
*
*   @param toWhat A string specifying which event to listen to.
*   @param fcn A function to call when the event happens.
*   @return A boolean value, true if the listener was successfully set. False
*           otherwise.
*/
AutoScrobbler.prototype.listen = function(toWhat, fcn) {

//Initialise the function register if not done already
if (typeof this.evtLstnrs._EVTLST_reg == "undefined")

this.evtLstnrs._EVTLST_reg = {};


if (this.evtLstnrs.hasOwnProperty(toWhat)) {

//Naming the function so we can remove it if required. Uses hasher.
var fcnName = this.hasher(fcn);

//Add the function to the list.
var event = this.evtLstnrs[toWhat];
event[event.length] = fcn;
this.evtLstnrs._EVTLST_reg[toWhat+"->"+fcnName] = event.length;
return true;

} else

return false;

}

/** Custom event listener trigger for events in AutoScrobbler
*
*   @param what Which event has happened.
*/
AutoScrobbler.prototype.trigger = function (what) {

if (this.evtLstnrs.hasOwnProperty(what)) {

var event = this.evtLstnrs[what];
for (var i = 0; i < event.length; i++)

event[i]();

}

}

/** Custom event listener removal for events in AutoScrobbler
*
*   @param toWhat A string to specify which event to stop listening to.
*   @param fcn The function which should no longer be called.
*   @return A boolean value, true if removal was successful, false otherwise.
*/
AutoScrobbler.prototype.unlisten = function(toWhat, fcn) {

var fcnName = this.hasher(fcn);
if (this.evtLstnrs._EVTLST_reg[toWhat+"->"+fcnName) {

var event = this.evtLstnrs[toWhat];
var fcnPos = this.evtLstnrs._EVTLST_reg[toWhat+"->"+fcnName];
event[fcnPos] = void(0);
delete this.evtLstnrs._EVTLST_reg[toWhat+"->"+fcnName];

return true;

}

return false;

}

/** Remove a native event listener regardless of the browser you're using.
*
*   @param eventElm The element that had been listening.
*   @param eventType The type of event that was being listened for.
*   @param callback The function that was set to trigger.
*   @return Undefined if successful, otherwise an exception will be thrown.
*/
AutoScrobbler.prototype.setNativeListener(eventElm, eventType, callback) {

if (eventElm.addEventListener)

eventElm.addEventListener(eventType, callback, false);

else if (eventElm.attachEvent)

eventElm.attachEvent('on' + eventType, callback);

else {

eventElm['on' + eventType] = callback;
if (typeof eventElm['on' + eventType] == "undefined")

throw "EventHandler Error: The event could not be set";

}

}

/** Set a native event listener regardless of the browser you're using.
*
*   @param eventElm The element to which the element to listen for, will involve.
*   @param eventType The type of event to listen for.
*   @param callback The function to call when the event happens.
*   @return Undefined if successful, otherwise an exception will be thrown.
*/
AutoScrobbler.prototype.removeNativeListener(eventElm, eventType, callback) {

if (eventElm.removeEventListener)

eventElm.removeEventListener(eventType, callback, false);

else if (eventElm.detachEvent)

eventElm.detachEvent('on' + eventType, callback);

else

eventElm['on' + eventType] = undefined;
if (typeof eventElm['on' + eventType] != "undefined")

throw "EventHandler Error: The event could not be removed";

}

/** A function which will inject a piece of HTML wrapped in a
*   <div> within any node on the page.
*
*   @param code The HTML code to inject.
*   @param where The node to inject it within.
*   @param extraParams An object which allows optional parameters
*   @param extraParams.outerDivId The id to be given to the wrapping <div>
*   @param extraParams.outerDivClass The class to be given to the wrapping <div>
*   @param extraParams.insertBeforeElm An element within the element given
*                                      in where, to insert the code before.
*/
AutoScrobbler.prototype.injectHTML(code, where, extraParams) {

if (typeof extraParams) {

if (extraParams.hasOwnProperty("outerDivId"))

var divId = extraParams.outerDivId;

if (extraParams.hasOwnProperty("outerDivClass"))

var divClass = extraParams.outerDivClass;

var insBefElm = (extraParams.hasOwnProperty("insertBeforeElm")) ? extraParams.insertBeforeElm : null;

}

var node = document.querySelector(where);
var elm = document.createElement('DIV');

if (divId)

elm.id = divId;

if (divClass)

elm.className = divClass

elm.innerHTML = code;
node.insertBefore(elm, insBefElm);

}

/** Starts the auto-scrobbler, scrobbles immediately and schedules an update
*   every 5 minutes.
*/
AutoScrobbler.prototype.start = function() {

this.loadThenAdd();
autoScrobbler.loopUID = setInterval(this.loadThenAdd, 300000);
autoScrobbler.start.disabled = true;
autoScrobbler.stop.disabled = false;

}

/** Stops the auto-scrobbler, ends the recurring update and zeros the required
*   variables.
*/
AutoScrobbler.prototype.stop = function() {

clearInterval(this.loopUID);
this.lastTrackUID = undefined;
this.loopUID = -1;
this.stop.disabled = true;
this.start.disabled = false;

}

/** Loads the new track list using Universal Scrobbler and schedules a scrobble
*   of the latest tracks 30 seconds afterwards.
*/
AutoScrobbler.prototype.loadThenAdd = function() {

doRadioSearch();
setTimeout(this.addLatest, 30000);

}

/** Selects all the tracks which have not been seen before and scrobbles them
*   using Universal Scrobbler.
*/
AutoScrobbler.prototype.addLatest = function() {

var tracks = document.getElementsByClassName(".userSong");
this.lastTrackUID = (typeof this.lastTrackUID == "undefined") ? tracks[1].getElementsByTagName("input")[0].value : this.lastTrackUID;

//Check every checkbox until the last seen track is recognised.
for (var i = 0; i < tracks.length; i++) {

var item = tracks[i];
if (item.getElementsByTagName("input")[0].value == this.lastTrackUID) {

i = tracks.length;
this.lastTrackUID = tracks[0].getElementsByTagName("input")[0].value;

} else {

item.getElementsByTagName("input")[0].checked = true;
this.scrobbled++;

}

}
doUserScrobble();
this.trigger("addLatest");

}

/** Updates the user interfaces to reflect new scrobbles.
*/
AutoScrobbler.prototype.reportScrobble = function() {

this.countReport.innerHTML = this.scrobbled;

}

// Create a new instance of the AutoScrobbler.
autoScrobbler = new AutoScrobbler();

This concludes my series of articles on refactoring code, what we’re left with is a much higher quality of code, which is cleaner, more efficient and easier to add to in the future. I’ll round this series off with a final “teardown” article to review and add some helpful tips which you may want to follow as you refactor your own code.

* It should be noted that, while 140medley does a brilliant job and will definitely solve these compatibility options, it won’t check to see if the feature is already natively in the browser. This means modern browsers will be using a workaround needlessly.

Advertisements

Making IE play nice…still

I came across an issue with IE9 yesterday, while using Twitter Bootstrap. I almost couldn’t believe it, having copied the template from the Bootstrap website, I thought it would be issue free. To add to my exasperation, the bug was in IE9 – the modern browser

At first I simply couldn’t understand what was wrong. Everything seemed to be correct, and then I looked at the IE developer tools. IE was rendering the page in quirks mode! Having used the HTML5 doc type, having not played with the styles in bootstrap, even having removed a few HTML comments I’d added, it was still happening!

Luckily, a guy that I was working with had seen the issue before, and before long he’d found a tag which sorted everything out. Rather than working fine with valid and correct HTML, IE9 needs this <meta> tag:

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

Once this had been put into the <head> of the document, everything rendered with the IE9 engine and all was well again. But it is still quite ridiculous to see some hack-like technique being required in any modern browser.

What’s worse is that we won’t be rid of IE9 any time soon. With the release preview of IE10 for Windows 7 only having been made available last month, it’s unlikely there will be a huge rush to upgrade. Especially as IE10 sports Windows 8 like gesture controls for a lot of operations, which will surely alienate most users of the previous browsers.

It seems that this is yet another IE-only hack that we’ll just have to endure for a little while more!

EDIT (09/01/2013): Looks like I may’ve over-looked part of the bootstrap documentation, it seems that this meta tag is indeed mentioned in the “Responsive design” section! Oops!

Useful Links

 Microsoft explaining their different rendering modes

 A stackoverflow question which was answered with this <meta> tag

Javascript: addEventListener and element.event

A quick post on the differences between addEventListener and element.event.

addEventListener and element.event, on first appearances, do the same thing. They allow a custom function or action to be linked to a javascript event, which are usually triggered by user-input.

But in more detail they do in fact have differences and for this reason, they should be used and treated differently. Each case where either is required should be assessed to make sure that it’s the best option of the two.

Using both methods

To use an addEventListener for the mousedown event, for example, we would do this.

var mouseInput = function(e) {
//filter the mouse inputs you're looking for
//do something based on the mouse input
}

document.addEventListener('mousedown', mouseInput, true);

The alternative element.event method would look like this.

var mouseInput = function(e) {
//filter the mouse inputs you're looking for
//do something based on the mouse input
}

document.onmousedown = mouseInput;

That’s it, and in most cases, both those will do the same thing…

The Exception to Explain the Difference

However, the methods are actually doing different things behind the things.

addEventListener

addEventListener assigns an additional function or action to carry out when the event happens. The clue is in the name really, addEventListener suggests that it won’t overwrite or override any other actions. This is generally a good thing, scripts, extensions and the browser itself will have assigned things to certain events and it may make the User Experience (UX) poorer if those normal functions and actions don’t happen when those specific events happen.

However, it also means that if you are purposely trying to change default actions to in fact improve the UX, this method won’t work “out-of-the-box”. An example of this is if you’re writing a web app and you want to force your own right click context menu*. You won’t surpress the context menu by default.

*A note on this idea: This is something you should carefully think about, it can certainly be useful, but many core browser operations can be found in the default right-click menu, and many extensions place a menu item to enable them to work effectively. It’s best to only use it on certain parts of the web page/app if you really are going to do this. It is very inadvisable to use this technique to stop people copying your content, there are other ways to take your content and it will often detriment genuine, non-malicious users.

element.event

element.event is an absolute assignment, it scraps and effectively destroys anything that was meant to happen when that event occurred and instead replaces it with yours. When put like that, it’s a pretty selfish way of coding as, again, it is likely to detriment the overal UX. Posts about making element.event assignments less problematic were commonly seen around a decade ago. But as addEventListener has become more supported over the years (it has been in the w3 spec for years and had support in Netscape 6(!), but only gained full support across the browsers when IE9 came in), and full featured libraries such as jQuery it has been less of an issue.

However, element.event has it’s place. It has much wider support. It’s likely that you’ll be able to support browser versions you’ve not even considered because this method of registering event listeners has been around since the beginning of Javascript (it’s fully supported in Netscape 2 as part of DOM 0…). So if compatibility is a real issue further than it is for most projects, and you can’t use a library (which will more than likely have this support built in), then this is your method to go for.

It also is quicker, and in some respects, a little easier to read, so if you’re writing a proof of concept or a quick script, where you know you won’t be interfering with anything unintentionally. Then this is also the method to choose.

The Best of Both

It is now accepted that using addEventListener over element.event is generally a better way to do things. You won’t be mucking about with the pre-assigned functions and actions for that event, which means that you’ll provide a better, more consistent UX overall.

However, if you actually are looking to surpress default actions using addEventListener, it is possible. This final example demonstrates how.

var mouseInput = function(e) {
e.preventDefault();
//filter the mouse inputs you're looking for
//do something based on the mouse input
}

document.addEventListener('mousedown', mouseInput, true);

It should be noted that this example is actually a very bad idea if implemented, as it will stop any pre-assigned click functions. If you were trying to prevent the default context menu from appearing on right click, you should replace mousedown with contextmenu to do this correctly. See my (*) about this above.

Further Reading

 StackOverflow thread that brought these differences to my attention
 Further information on element.event registration
 Article from 2004 on making a cross-browser addEventListener (for the onload event) function for full browser support
 Article from 2001 about how difficult it was to code cross-browser event handlers (not much has changed sadly…)
 Mozilla Developer Network page on both event registration methods
 QuirksMode article on DOM Level 0 (the first version of DOM, used in the first iterations of Javascript)