This is article number 5 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.
Cleaning Up Our Messy User Controls
We’re pretty close to being done here in terms of refactoring our Javascript. But the Auto-Scrobbler has a UI, an incredibly simple UI, but it still has one, and because this is a bookmarklet for a third-party website, the only way I can show this UI is by injecting the HTML on to the page. This not always the best way to do things, often it’s suggested that just the data is entered into pre-existing HTML on the page, but in this case it’s about the only thing we can do. But there are still best practices that come with this.
Here’s the current HTML that we inject into the page (post-injection):
<div id="autoScrobbler" style="background: #FFFFFF; border-top: 1px solid #000000; border-left: 1px solid #000000; position: fixed; bottom: 0; height: 50px; width: inherit;"><input id="autoScrobblerStart" type="button" value="Start auto-scrobbling" onclick="autoScrobbler.start();" /> | <input id="autoScrobblerStop" type="button" value="Stop auto-scrobbling" onclick="autoScrobbler.stop();" /><p><span id="autoScrobblerScrobbleCount">0</span> tracks scrobbled</p></div>
This is a horribly messy bit of HTML, having originally written this code quickly I haven’t even included any structure, it’s all just one flowing line. To make things worse I am using inline styles and, worse still, inline event handlers. The way I’m injecting the code isn’t great either, I’m adding it to the end of another element, which means the styles are liable to change and possibly break the UI.
Structure
That’s actually a lot of stuff to turn around, so we’ll just start with the structure. To get some structuring, we need to use the new line character at the end of each line (“\n” in most languages) in our Javascript. Like so:
var userControls = "<div id=\"autoScrobbler\" style=\"background: #FFFFFF; border-top: 1px solid #000000; border-left: 1px solid #000000; position: fixed; bottom: 0; height: 50px; width: inherit;\">\n"+
"<input id=\"autoScrobblerStart\" type=\"button\" value=\"Start auto-scrobbling\" onclick=\"autoScrobbler.start();\" /> | <input id=\"autoScrobblerStop\" type=\"button\" value=\"Stop auto-scrobbling\" onclick=\"autoScrobbler.stop();\" />\n"+
"<p><span id=\"autoScrobblerScrobbleCount\">0</span> tracks scrobbled</p>\n"+
"</div>\n";
Which should inject something like this:
<div id="autoScrobbler" style="background: #FFFFFF; border-top: 1px solid #000000; border-left: 1px solid #000000; position: fixed; bottom: 0; height: 50px; width: inherit;">
<input id="autoScrobblerStart" type="button" value="Start auto-scrobbling" onclick="autoScrobbler.start();" /> | <input id="autoScrobblerStop" type="button" value="Stop auto-scrobbling" onclick="autoScrobbler.stop();" />
<p><span id="autoScrobblerScrobbleCount">0</span> tracks scrobbled</p>
</div>
Better, but those inline styles and event handlers are still making things messy.
Styling
We’ll solve the inline styles first.
It should first be mentioned that those inline styles aren’t just messy but also frustrating. Inline styles are generally the most specific you can get with CSS to styling elements, which makes sense, by putting styles inline, they will only ever be applied to that element unlike any other CSS that you can apply. But what it means is that another developers, or even I, myself trying to extend the bookmarklet in some way, won’t be able to alter the styling in any way without either changing the original source code or using “!important” after my rule. Using these rules can get messy because it’s only going to lead down a messy road of adding secondary or even tertiary “!important” rules to allow developers to get the result they’d like.
So we won’t even start going down that route, we’ll instead include a separate stylesheet with these styling rules. This should look something like…
.auto-scrob-cont {
position:fixed;
bottom: 0;
width: inherit;
min-height: 50px;
background: #FFFFFF;
border-top: 1px solid #000000;
border-left: 1px solid #000000;
}
Simple enough, and to load it in, we’ll use a similar function to the one that’s used to load in the bookmarklet in the first place (which is based on some of the code used in an article at betterexplained.com):
/** 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);
}
We’ll add this into our javascript, but this will essentially call a second file with the CSS above. This now means that there’s a certain flexibility, I can have several instances of the UI (maybe showing different bits of information for instance) around the page. To do this I’ve added to classes to each of the main HTML elements, as using the ID attribute to select the elements in CSS would eliminate that possibility of having several instances, it also means we can have more general names for elements, such as “start” and “stop”.
<div id="autoScrobbler" class="auto-scrob-cont">
<input id="autoScrobblerStart" class="start" type="button" value="Start auto-scrobbling" onclick="autoScrobbler.start();" /> | <input id="autoScrobblerStop" class="stop" type="button" value="Stop auto-scrobbling" onclick="autoScrobbler.stop();" />
<p class="status-report"><span id="autoScrobblerScrobbleCount">0</span> tracks scrobbled</p>
</div>
Event Handling
Next we’ll fix those inline event handlers. Along with making the HTML more messy, inline event handlers are an old method of reacting to different events, it goes against unobtrusive javascript because we’re pushing javascript into the HTML, rather than keeping each language separate.
The way we do this is with event handlers, in the previous article I actually wrote a custom event handler mechanism to side-step having to deal with the compatibility issues which come with using event handlers (which are only doubled when trying to create custom events like I was previously). Here though, we can’t get away from using them if we want to improve our code. My gripe with eventListeners is compatibility, but as that’s the title of the next article, I’ll continue side-stepping the compatibility issues which exist. For now, we’ll just implement the “modern browser” method, but I’ll put it into a new function so we can address the compatibility issues more easily later.
/** Set an 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 True if listener was set successfully, false otherwise.
*/
AutoScrobbler.prototype.setNativeListener(eventElm, eventType, callback) {
eventElm.addEventListener(eventType, callback, false);
}
//Implemented like this
this.setNativeListener(this.startElm, ‘click’, this.start);
Okay so now with all that stripped out, we have some much cleaner HTML. It’s far easier to read than before, and we can assume what kind of thing the complementing Javascript will do to the elements.
<div id="autoScrobbler" class="auto-scrob-cont">
<input id="autoScrobblerStart" class="start" type="button" value="Start auto-scrobbling" /> | <input id="autoScrobblerStop" class="stop" type="button" value="Stop auto-scrobbling" />
<p class="status-report"><span id="autoScrobblerScrobbleCount">0</span> tracks scrobbled</p>
</div>
Code Injection
My final stage for this article is actually moving back to the Javascript code (I did say we were “nearly” there with it). As I said in the introduction to the article, the injection method I use is fine, but not perfect, it inserts it into another element on the page, which is dangerous, not only could that element be deleted, but the styles could change or even the structure of the page could change! It relies on the external code too heavily when it needn’t.
I’ll make an HTML injection method for the class, and I’ll make it pretty generic so that any HTML can be injected onto the page. This means that it’s a bit future-proofed, if we want to inject several bits of UI on the page, that facility is there!
/** 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);
}
//To implement we use
this.injectHTML(userControls, "#mainBody");
Okay, we now have clean HTML, CSS, event handling and code injection. Just for reference, here’s our full code.
/** 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;
}
/** 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);
}
/** 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.querySelectorAll(".userSong");
this.lastTrackUID = (typeof this.lastTrackUID == "undefined") ? tracks[1].querySelector("input").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.querySelector("input").value == this.lastTrackUID) {
i = tracks.length;
this.lastTrackUID = tracks[0].querySelector("input").value;
} else {
item.querySelector("input").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();
That’s it for this article, next we’ll be looking at achieving as full as possible compatibility.
