Keep your scope – jQuery.proxy

Wer kennt das nicht, man arbeitet mit jQuery und einem click-Event, und „verliert“ dadurch bei Objekten das „this“. Kleines Beispiel:

function MyObject() {
	this.hello = "Hello";
}

MyObject.prototype.clickHandler = function(event) {
	alert(this.hello);
};

var obj = new MyObject();

$('#clickMe').click(obj.clickHandler);

Wenn man jetzt auf ein gedachtes Element mit der ID „clickMe“ klickt, dann erscheint eine Fehlermeldung in JavaScript bzw. eine Alert-Box mit dem Inhalt „undefined“ – obwohl this.hello eigentlich doch „Hello“ sein sollte? Bei den jQuery Event-Handlern „verliert“ die Funktion ihr aktuelles Scope und das this zeigt dann auf den Sender des Events, in diesem Fall auf das Element mit der ID „clickMe“. Das ist zwar oft gewünscht, da man dieses nun zB mit $(this).text(‚Ich wurde geklicked‘); weiterverarbeiten kann, ist aber in meinem Beispiel oben sehr ungünstig. Deshalb bietet die jQuery-Library eine Funktion jQuery.proxy, welche eine Proxy-Funktion generiert mit der wir das Scope einer Funktion selbst beeinflussen können. Ersetzen wir Zeile 11 also mit folgendem:

$('#clickMe').click($.proxy(obj.clickHandler, obj));

Nun zeigt das this wärend unserem clickHandler-Aufruf wieder auf das eigentliche Objekt und eine Alert-Box mit dem Inhalt „Hello“ erscheint. Möchte man nun trotzdem auf das auslösende Element für das Event zugreifen, schafft

var calledBy = event.target;
// beispiel
$(calledBy).text("Link geklickt!");

abhilfe! Natürlich kann man auch seine eigene Proxy-Funktion mit direct-calling bauen, was aber den Code schon deutlich unübersichtlicher macht, allerdings viel mehr Flexibilität bietet (wieder Bezug auf 1. Beispielcode):

$('#clickMe').click((function(o) {
	return function(event) {
		o.clickHandler(event);
	};
})(obj));

Es lohnt sich also immer auch mal unter „Misc“ bzw. „Utilities“ in der Dokumentation einer Library nachsehen 😉

 

AJAX und der „Vor“ und „Zurück“ Button

Kleiner Hinweis: Dieser Blogpost nimmt an, das jQuery verwendet wird 😉

Dank AJAX kann man eine Menge Traffic einsparen – allerdings hat AJAX leider ein paar Nachteile: Die Browsernavigation, also die „Vor“ und „Zurück“ Knöpfe des Browsers, funktionieren nicht mehr.
Hier mal ein Beispiel:

HTML:

Inhalt ändern
Hier ist Inhalt 1

JavaScript:

$("a").live("click", function (e) {
    e.preventDefault();
    
    $('#content').load(e.target.href, function() {
        // nothing here yet ;)
    });
});

Klickt man nun auf „Inhalt ändern“, erscheint der Inhalt von content.html im content-div. Wenn jetzt der Benutzer allerdings wieder „Hier ist Inhalt 1“ sehen möchte, kann er nicht den „Zurück“-Button seines Browsers verwenden.

Um dieses Problem zu lösen gibt es zwei Möglichkeiten:

  • Einen hash an die aktuelle URL anhängen, etwa so: /#!content.html
  • Das HTML5 window.history-Objekt bemühen

Da die zweite Methode leider (noch) nicht von jedem Browser unterstützt wird, aber die wesentlich hübschere Methode ist, habe ich mir ein Objekt geschrieben, was beides kann!

/**
 * navigatorHistory
 *
 * Fixes navigator history for ajax-based sites
 *
 * @author agrafix 
 */
var navigatorHistory = {
	/**
	 * Title of site
	 */
	siteTitle: '',
	
	/**
	 * function to load content, needs to be set before hook()-call
	 */
	loaderFunc: null,

	/**
	 * check if the browser supports pushState
	 */
	isSupported : function() {
		return (typeof (window.history.pushState) == 'function');
	},

	/**
	 * add url to history
	 */
	add : function(url) {

		if (this.isSupported()) {
			
			window.history
					.pushState(null, this.siteTitle, url);
		} else {
			window.location.hash = "#!" + url;
		}
	},

	/**
	 * listen for back-forward button events
	 */
	hook : function() {
		if (!this.loaderFunc) {
			alert("No loaderFunc defined!");
			return;
		}
	
		if (this.isSupported()) {
			// doesn't work with jquery, no idea why :O
			window.addEventListener("popstate", function(e) {
				navigatorHistory.loaderFunc(document.location.pathname, true);
			});
		} else {
			$(window).bind('hashchange', function() {
				var location = (window.location.hash).replace(/^#!/, '');
				navigatorHistory.loaderFunc(location, true);
			});
		}

	}
};

Die Benutzung in Verbindung mit dem obigen HTML-Schnipsel ist denkbar einfach:

// funktion zum laden der Seiten definieren
navigatorHistory.loaderFunc = function(url, nohistory) {
	if (!nohistory) {
		navigatorHistory.add(url);
	}
	
	$('#content').load(url);
};

// seiten titel
navigatorHistory.siteTitle = "Deine Seite";

// auf vor bzw. zurück-button klicks hören
navigatorHistory.hook();

// die eigentliche ajax-funktion
$("a").live("click", function (e) {
    e.preventDefault();
	
	navigatorHistory.loaderFunc(e.target.href);
});

Eigentlich relativ einfach einzubauen und es macht eine Seite deutlich benutzerfreundlicher!

 

JavaScript Timers mit jQuery

Timer in JavaScript zu realisieren ist eigentlich recht leicht, dazu braucht mal nur die Funktionen window.setTimeout bzw. window.setInterval mit entsprechenden Argumenten zu füttern.

// funktion verzögert aufrufen
function machWas() {
   alert('alert() mit 1000ms verzögerung!');
}
window.setTimeout(machWas, 1000);

// funktion alle X Millisekunden aufruden
function machWasOft() {
   alert('alle 1000ms ein alert()');
}
window.setInterval(machWasOft, 1000);

Doch was ist, wenn ich jetzt einen solchen Timer ändern oder abbrechen möchte? Das ist eigentlich auch nicht schwer – die Funktionen setTimeout/setInterval geben ein Objekt zurück über das sie sich wieder beenden lassen.

var x = 0;
function machWasAbbrechbarOft() {
    x += 1;
    if (x > 200) {
        window.clearInterval(running); // beende das "Interval" nach 201 Ausführungen
    }
}
var running = window.setInterval(machWasAbbrechbarOft, 1000);

Das funktioniert in kleinen JavaScript Codes sicher ohne Probleme, wenn man allerdings an einem großem JavaScript Projekt arbeitet, macht es die Arbeit nicht gerade leichter wenn man immer dieses Rückgabe-Objekt (im Beispiel running) irgendwo speichern muss, vorallem wenn man von überall im Code darauf zugreifen will.

Um dieses Problem zu lösen habe ich ein jQuery Timer Plugin gefunden. (Siehe http://plugins.jquery.com/project/timers) Wer an einem größerem JavaScript Projekt arbeitet, sollte meiner Meinung nach aufjedenfall auf ein Framework setzen – jQuery ist hier meine Wahl von daher bietet sich dieses Plugin an.

Mit diesem Plugin geht das „Interval“ setzen dann wie folgt:

$(document).everyTime(1000, "machWasOftAbbrechbar", function(i) { // alle 1000ms
  alert('Hallo ich bin der ' + i + 'te Aufruf!'); // machWasOftAbbrechbar, hier die funktion die aufgerufen werden soll
}, 200); // 200 mal ausführen

Der zweite Parameter, machWasOftAbbrechbar, stellt hierbei ein sogenanntes „label“ dar, über das man den gestarten Timer dann wieder abbrechen kann:

$(document).stopTime("machWasOftAbbrechbar"); // stoppe machWasOftAbbrechbar

Viel mehr gibt’s eigentlich nicht dazu zu sagen – die Dokumentation dazu findet man, genau wie das Plugin selbst, unter http://plugins.jquery.com/project/timers.

 

jQuery in Greasemonkey

Wer kennt das nicht? Man möchte ein Greasemonkey-Script schreiben und würde dabei gerne auf jQuery zurückgreifen. Außerdem verwendet die betreffende Seite bereits jQuery, das heißt man müsste dieses Script eigentlich gar nicht mehr nachladen. Doch wie kann man auf die bereits vorhandene jQuery-Instanz zugreifen? Ganz einfach:

var $ = unsafeWindow.jQuery;

Das steht zu beginn des Scripts und nun kann man wie immer per $ auf alle jQuery Funktionen und Elemente der Seite zugreifen.

In diesem Zuge möchte ich gleich noch zwei neue UserScripts für das Browserspiel Die-Stämme vorstellen, die ich heute veröffentlicht habe und welche ebenfalls auf die genannte Technik zurückgreifen:

[DS] Save Attack

Ermöglicht es im Versammlungsplatz die gewählen Einheiten zu speichern um bei einem weiteren Angriff mit gleichen Truppen anzugreifen

Download & Install

[DS] Menu Resort

Dieses Script sortiert die ingame Die-Stämme Menüleiste so um wie sie früher war (vor Version 7.x)

Download & Install