ek

Beiträge zum Thema JS, HTML, CSS & anderem Kram

(English version of this article @Codepen.)

Neulich hatte ich ein interessantes Problem: Ich musste ein <iframe>-Element in eine responsive Website einbetten, dessen Inhalt wiederum nicht responsiv war. Man kann sich leicht vorstellen, wo hier der Fallstrick liegt. Wird der Bildschirm kleiner, passt sich die umgebende Website zwar problemlos an, der Inhalt des iFrames jedoch bleibt starr in seinen Dimensionen. Zusätzlich erscheinen unschöne, horizontale Scroll-Balken am iFrame.

Glücklicherweise gibt es in modernen Browsern einen Weg, mit diesem Problem umzugehen. Mithilfe von CSS-Transforms kann der iFrame inklusive seines Inhalts runter skaliert werden wie ein Bild. Der folgende Codepen zeigt, wie das aussehen kann.

(Link zur Demo-Seite)

Um dieses Verhalten zu implementieren, benötigen wir als erstes einen Breakpoint, ab dem die Skalierung wirksam wird. Den Breakpoint kann man sehr leicht ermitteln, indem man das Browser-Fenster verkleinert. Sobald am iFrame ein horizontaler Scroll-Balken auftaucht, hat man den Breakpoint erreicht.

JavaScript

var BREAKPOINT = 1060; // (Willkuerlich gewaehlter Wert!)

Der Breakpoint dient außerdem als Referenz-Breite des iFrames: 100% entsprechen 1060px. Nun benötigen wir den Skalierungs-Faktor. In der Theorie ist das der Quotient aus gegenwärtiger Bildschirm-Breite und iFrame-Referenz-Breite. In der Praxis hat sich dieser Wert für mich jedoch ab einem bestimmten Punkt als zu groß erwiesen. Daher verringere ich den Wert geringfügig mithilfe der Math.pow-Methode und einem Exponenten von 1.2.

JavaScript

var scale = Math.pow(window.innerWidth / BREAKPOINT, 1.2);

Eigentlich habe wir damit auch schon alles, was wir benötigen: einen Breakpoint, ab dem das Skalieren einsetzt, und einen Skalierungs-Faktor. Setzten wir nun jedoch den CSS-Transforms-Wert mit dem jeweiligen Skalierungs-Faktor am iFrame-Element an, passiert etwas merkwürdiges: der Inhalt des iFrames skaliert unmerklich, wobei das iFrame-Element selbst rapide an Größe verliert und buchstäblich in sich zusammen fällt. Das ist leider nicht das gewünschte Resultat.

Glücklicherweise gibt es einen Weg, dieses Verhalten zu unterbinden. Die Lösung ist, die Dimensionen des iFrames in dem Maße zu erhöhen, in dem es skaliert wird. Die Auswirkungen der CSS-Transforms auf die Abmessungen werden somit vollständig kompensiert und der iFrame verhält sich wieder wie ein normales, responsives Element. Lediglich der Inhalt des iFrames schrumpft wie gewünscht.

JavaScript

var scale = 0.8; // (Willkürlich gewählt!)
var width = 100 / scale; // Breite in Prozent
var height = INITIAL_IFRAME_HEIGHT / scale; // Hoehe in Pixeln

Die Breite des iFrames in Prozent ist die Inverse des gegenwärtigen Skalierungs-Faktors multipliziert mit 100. Die angepasste Höhe in Pixel ist der Quotient aus initialer Höhe und Skalierungs-Faktor. Die initiale Höhe des iFrames muss dabei ganz zu Beginn einmal ermittelt und gespeichert werden.

Jetzt gibt es nur noch ein potenzielles Problem: ist das iFrame-Element mithilfe von margin: * auto; mittig auf der Seite positioniert, muss die transform-origin ebenfalls durch "center top" auf Zentrierung eingestellt werden.

Dies hat jedoch zur Folge, dass der iFrame während des Skalierens rechts aus dem Bild "läuft", da sich seine Breite sukzessive erhöht. Um dieses Verhalten zu kompensieren, muss der iFrame um die Hälfte seines Wachstums in der Breite nach links verschoben werden.

JavaScript

var width = 100 / scale; // Breite in Prozent
var offsetLeft = (width - 100) / -2; // Versatz nach links in Prozent

Setzt man diesen Versatz mithilfe von transform: translateX(…);, bleibt der iFrame in der Mitte der Seite.

Das ist auch schon alles. Solltest du Fragen dazu haben, schreib einen Kommentar oder frag mich via Twitter: @Herschel_R.

Responsive Webdesign (kurz: RWD) kann man getrost als old News bezeichnen und mittlerweile weiß jeder, worum es beim Responsive Webdesign geht. Sollte man meinen.

Meine Recherche nach einem Drupal-Template am gestrigen Abend hat mich jedoch ziemlich ernüchtert und mich an eine BarCamp-Session erinnert, wo der Vortragende über Responsive Webdesign erzählen wollte, jedoch nur eine kleine Einführung in Media Queries gab und sich anschließend im Glauben wähnte, das Thema angemessen erschlagen zu haben. Doch beschränkt sich der RWD-Ansatz mitnichten nur auf den Einsatz von Media Queries.

Aber zurück zu meiner Recherche: Wie es heutzutage beim intensiven Betrachten einer Website Gang und Gäbe ist, habe ich mir jeweils mit dem Cursor den linken Browser-Rand geschnappt und das Fenster horizontal auf und zu gezogen. Das hat sich in den letzten Jahren zu einer regelrechten Neurose in Webdesigner-Kreisen entwickelt. Doch liegt der Nutzen auf der Hand: Man kriegt eine Demonstration darüber, wie sich die Seite ungefähr auf kleineren Bildschirmen verhält.

Dabei war das Resultat in den meisten Fällen das gleiche: Der Inhalt schreckte angsterfüllt zurück sobald der Fensterrand näher kam und kauerte sich nahezu klaustrophobisch in die Seitenmitte. Nur, um nach kurzer Zeit abermals zusammen zu zucken und in neuerliche, geschrumpfte Hab-Acht-Stellung zu verfallen. Da ich einen "Auftrag" hatte und keine Lust, mich mit diesen "Schmuddelkindern" des RWDs intensiver auseinander zu setzen, habe ich die Breakpoints in den Media Queries nicht näher untersucht.

Ich würde jedoch Geld darauf wetten, dass da einfach anhand der Auflösung aktueller iPhones und iPads die Breakpoints gesetzt und das Layout neu-definiert wurde. Das ist auch akzeptabel für Besucher mit "Referenz"-Geräten. Wer jedoch das Pech hat, bspw. mit einem Tablet auf die Seite zu kommen, das eine geringfügig kleinere Auflösung als das iPad hat, bekommt das iPhone-Layout angezeigt: ein Klopapier-breiter Inhaltsstreifen in Gesellschaft zweiter dicker Whitespace-Bereiche links und rechts.

Hier lässt sich der Effekt gut beobachten (der Name des Templates grenzt an Hohn).

Schön und gut, denkt sich da vielleicht der/die geneigte Leser/in. Toll ist das nicht, aber wie soll man es denn dann machen?

"Nichts leichter wie dies!", um es mit den Worten Helge Schneiders zu sagen. Man schmeißt die lästigen Pixel-Angaben aus seinem Stylesheet und macht Breiten-Angaben sowie horizontale Abstände in Prozent-Angaben. Zusätzlich gibt man eine Maximal-Breite der Seite an, damit diese bspw. nicht auf 24-Zoll-Bildschirmen den kompletten horizontalen Raum einnimmt. In den Media Queries ergänzt man anschließend noch Angaben zur Anpassung des Layouts der Elemente. Das Resultat ist ein Design, dass sich geschmeidig wie eine junge Katze dem Browser-Fenster anpasst, egal, wie breit es ist.

Hier lässt sich das ganze praktisch erfahren (kurioserweise ist das Professional Theme unter RWD-Gesichtspunkten besser als das Best Responsive Theme …)

Dem Design ist es nun egal, mit welchem Gerät man die Seite besucht. Die Breakpoints wurden auch nicht nach Lieblingshersteller gesetzt, sondern anhand des Inhalts. Unterstützt werden nicht eine handvoll Auflösungen, sondern ein stufenloser Bereich zwischen einer Minimal- und einer Maximalauflösung.

Und warum habe ich diesen Artikel geschrieben? Ganz einfach, um Missverständnisse aus der Welt zu räumen. Für diejenigen, die nicht selbst HTML/CSS schreiben, jedoch auf fertige Themes/Templates zurückgreifen und da etwas responsives suchen, der Hinweis, genau zu prüfen, was einem da als "responsive" angeboten wird. Euren Besuchern auf Mobile Devices zuliebe solltet ihr auf pseudo-responsive, fixe Layouts verzichten und richtige, responsive Fluid-Layouts wählen. So wird euer Inhalt immer anständig dargestellt, nicht nur auf drei Geräten. Den Unterschied sieht man im sogenannten Browser-Fenster-Auf-und-Zuzieh-Test.

Und für diejenigen, die selbst entwickeln und beim Thema Responsive Webdesign bisher als erste Assoziation Media Queries hatten, die Bitte, sich des Themas umfassender anzunehmen. RWD ist ein neuer Ansatz abseits fixer Layouts. Media Queries sind dabei nur ein Teilaspekt. Folgende zwei Bücher sind ein großartiger Einstieg in das Thema:

  1. Responsive Webdesign: Reaktionsfähige Websites gestalten und umsetzen | neu, umfassend und auf deutsch
  2. Responsive Web Design | der Klassiker von Ethan Marcotte

So, das musste ich einfach mal loswerden. Fragen, Anregungen, Ergänzungen sind hoch willkommen. Scheib einfach einen Kommentar.

Github – herschel666 – Drop-a-Site

“Drop-a-Site” auf GitHub | Download

Hin und wieder kommt man in die Verlegenheit, eine kleine statische Seite aufsetzen zu müssen. Diese ist vom Umfang her so dünn, dass es sich nicht lohnt, eine Datenbank anzulegen und das Standard-CMS/-Framework der Wahl zu strapazieren. Andererseits hat man aber auch keine Lust, mit einem Haufen statischer HTML-Dateien rum zu hantieren.

Das ist der Punkt, wo so genannte “Static Site Generators” ins Spiel kommen. Ein Vertreter dieser Gattung möchte “Drop-a-Site” sein – einfachste Bereitstellung statischer Inhalte mithilfe von URL-Rewriting. Dabei habe ich Wert auf Simplizität gelegt. Man definiert die Pfade zu seinen Seiten sowie die zugehörigen Seitentitel, und “Drop-a-Site” liefert aus. Auf weiterführende Features habe ich bewusst verzichtet.

Die Einrichtung von “Drop-a-Site” ist denkbar einfach:

1. Seiten-URL definieren

Die nötigen Anpassungen müssen in der config.php vorgenommen werden. In der SITE_URL-Konstante wird der vollständige URL der Seite gespeichert. Liegt die Seite nicht im Wurzelverzeichnis sondern in einem Unterordner, muss das mit eingetragen werden.

2. Unterseitepfade und -titel definieren

Im $pages-Array werden die Pfade zu den Unterseiten sowie deren Titel gespeichert. Das URL-Rewriting sorgt dafür, dass die Seiten über den eingetragenen Pfaden erreichbar sind. Unter- und Unter-Unterseiten stellen kein Problem dar.

3. Anpassung für den Fall, dass die Seite nicht in der Wurzelebene liegt

Liegt deine Seite in der Wurzelebene, kannst du diesen Punkt getrost überspingen.

Liegt die zu erstellende Seite jedoch in einem Unterordner, muss die Deklaration der $req-Variable angepasst werden. Genauer gesagt, muss der Pfad zur Unterseite aus dem Request-String entfernt werden. Ist die Seite bspw. über die Adresse http://example.com/foo/bar/ erreichbar, muss die Deklaration der $req-Variablen folgendermaßen aussehen:

PHP

$req = trim(str_replace('/foo/bar/', '', $_SERVER['REQUEST_URI']), '/');

4. Anpassen der .htaccess-Datei

Auch diesen Punkt kannst du überspringen, wenn deine Seite in der Wurzeleben liegt.

Andernfalls musst du dem mod_rewrite mitteilen, in welchem Unterordner die Seite liegt, damit es fehlerfrei funktionieren kann. Dafür gibt es in der .htaccess den Wert RewriteBase. Ausgehend von der Annahme, die Adresse deiner Seite ist http://example.com/foo/bar/, muss der Eintrag in der .htaccess so aussehen:

PHP

RewriteBase /foo/bar/

5. Einstellen der Inhalte

Schlussendlich müssen nur noch die Inhalte eingetütet werden. Diese werden im Ordner “inc” gelagert und nach den entsprechenden Seiten benannt. Zur Seite “about” gehört also die Datei “about.inc.php”. Für Unterseiten müssen entsprechende Ordner innerhalb von “inc” angelegt werden.

Zusätzlich gibt es innerhalb von “inc” den “components”-Ordner - in diesem können Template-Parts wie Header, Footer, Navigation, etc. ablegt werden.


Das ist auch schon alles, was es zu tun gibt, um “Drop-a-Site” in Betrieb zu nehmen. Natürlich kann das System nach Belieben erweitert und ausgebaut werden. Allerdings muss man dabei abwägen, ob es den Aufwand wert ist.

Bei Fragen oder Anregungen sei an die Kommentarfunktion verwiesen.

“Drop-a-Site” auf GitHub | Download

Require.js-Autoloader

Wenn aus dem “One-Pager” ein “Multi-One-Pager” wird, kann sich ein gewisser Overhead bezüglich der geladenen Skripte einstellen. Angenommen man baut eine Web-App, die aus vielen Einzelseiten besteht, und jede Einzelseite stellt für sich einen “One-Pager” dar, welcher nicht zwingend beim Besuch eines Nutzers aufgerufen wird. Dann werden – benutzt man Require.js zum Laden der Skripte – beim initialen Aufruf der Web-App alle Skripte geladen, auch wenn sie letztlich gar nicht benötigt werden.

In diesem Fall wäre es praktischer, mehrere Require.js-Instanzen anzulegen und durch einen zentralen Router gesteuert bei Bedarf aufzurufen. Dieses Szenario habe ich einmal mithilfe von Backbone.js nachgestellt. Die “Web-App” besteht aus diversen Einzelseiten, welche bestimmte jQuery-UI-Widgets beinhalten. Außerdem gibt es eine Startseite mit einem kurzen Erklärungstext. Ziel ist es, den Startseiteninhalt und die jQuery-UI-Widgets nur bei Bedarf zu laden.

Dafür wird beim Aufruf der Seite lediglich eine fundamentale Require.js-Instanz aufgerufen, die die global benötigten Skripte – jQuery, Underscore, Backbone, den jQuery-UI-Core und einen Observer – laden und einen Backbone-Router initialisieren, der die Autoloader-Funktionalität zur Verfügung stellt.

JavaScript

require(['jQuery', 'Underscore', 'Backbone', 'Observer'], function ($, _, Backbone, Observer) {

	var Router = Backbone.Router.extend({

		routes : {
			':site/*sub' : 'autoLoad'
		},

		initialize : function () {
			location.hash = location.hash || 'index/';

			Backbone.history.start();
		},

		autoLoad : function (site, sub) {
			var path = 'sites/' + site + '/app';

			require([path], function (App) {
				App.init({
					subPages : sub
				});
			});
		}
	}),

	router = new Router();

});

Der Router prüft als erstes, ob ein Location-Hash gesetzt ist. Ist dies nicht der Fall, wird der Hash “/index/” gesetzt, was die Initialisierung der Startseite einleitet. Weiterhin wartet der Router auf Änderungen des Location-Hash und führt im Bedarfsfall die “autoLoad”-Funktion aus, welche das gewünschte Modul/die gewünschte Seite lädt. Das “*sub” im “routes”-String des Routers stellt sicher, dass auch beim initialen Aufruf von Unter-Unterseiten die Autoload-Funktion tut, was sie soll.

Das vom Autoloader geladene Skript übergibt eine “init”-Funktion – dabei kann es sich, je nachdem ob die Seite lediglich statischen Inhalt hat oder tiefergreifende Funktionalität besitzt, entweder um eine Backbone-View- oder eine Backbone-Router-Instanz handeln.

JavaScript

var init = function () {
		return new View();
	};

return {
	init : init
};

JavaScript

var init = function (args) {
		return new Router(args);
	};

return {
	init : init
};

Das Datepicker-Modul habe ich als Beispiel für eine zusätzliche Router-Instanz gewählt. Inhaltlich zugegebenermaßen etwas mau, wird doch deutlich, dass auf diese Weise die Funktionalität der Web-App auch auf Unterebenen gewährleistet werden kann, ohne dass dies beim initialen Aufruf der Seite angemeldet bzw. geladen werden muss.

Beobachtet man den Inhalt des HTML-Head beim Durchklicken der Seite im Firebug bzw. Web Inspector, kann man sehr schön verfolgen, wie die jeweils benötigten Module und Widgets sukzessive nachgeladen werden.

Ich hoffe, mein Versuch, den Sachverhalt hier darzustellen, ist einigermaßen verständlich. Über Anregungen, Anmerkungen und Fragen freue ich mich immer. Sollte diesbezüglich Bedarf bestehen, kann ich nur dazu ermutigen, regen Gebrauch von der Kommentar-Funktion zu machen.

Außerdem sei darauf hingewiesen, dass der Code zur schnellen Inspektion auch auf GitHub zur Verfügung steht.

Clipboard – Digitales Notizbrett mit Node.js und Backbone.js

Um etwas Zerstreuung vom Uni-Lernstress zu bekommen, habe ich einen Ausflug in die bunte Node.js-Welt unternommen. Als Mittel zum Zweck diente mir dabei das digitale Notizbrett “Clipboard”. Serverseitig kommt das Application-Framework Express zum Einsatz. Gespeichert werden die Daten in einer MongoDB-Datenbank mithilfe von Mongoose.

Client-seitig kommt die bewährte Kombo aus Underscore, Backbone und jQuery zum Einsatz. Für Ordnung sorgt überdies Require.js, wobei ich auf folgenden Ansatz zurückgegriffen habe: Modular JavaScript & Backbone.js. Außerdem hat jQueryUI einen “Gast-Auftritt” und sorgt für die Draggability der Notizen.

Das Hosting übernimmt dankenswerterweise AppFog, die Daten werden bei MongoHQ abgelegt. Defenitiv zwei sehr praktische Services.

(Clipboard-Code bei GitHub)

Rollstuhlsport – Android-App des DRS

Der Deutsche Rollstuhl-Sportverband hat neuerdings eine Android-App, welche ich in meiner Rolle als Frontend-Developer bei w3design. entwickeln durfte. Die App wird zeitnah außerdem auch für das iPhone erscheinen.

Was daran jedoch besonders interessant ist – abgesehen vom Erscheinen der App selbst –, ist die Tatsache, dass ich sowohl von Java, als auch von Objective-C nicht den blassesten Schimmer habe. Vielmehr habe ich mich beim Entwickeln der App auf meine HTML-, CSS- und JavaScript-Kenntnisse verlassen. Möglich ist das durch PhoneGap, ein Framework, das Web-Apps in native Apps umwandelt und dabei ein paar sinnvolle Hardware-APIs zur Verfügung stellt.

Für die App selbst, habe ich auf Backbone.js gesetzt, da es mir gute Möglichkeiten bot, einerseits die Fülle an unterschiedlichen Daten zu verarbeiten, und andererseits ein sehr umfangreichen “One-Pager” zu bauen, welcher aus einer index.html besteht und alle Unterseiten client-seitig rendert.

Ich kann Phonegap jedenfalls nur empfehlen und möchte noch auf LungoJS hinweisen, welches bei kleineren Apps sicher gut mit Phonegap Hand in Hand geht.

Derzeit bin ich mit dem Bau einer Web-App beschäftigt, die initial einen Haufen Daten asynchron laden soll. Dafür könnte ich zwar ganz einfach ein paar $.ajax-Funktionen nacheinander aufrufen, doch hat diese Methode ihre Nachteile:

  1. Es bestehen Abhängigkeiteen zwischen den zu ladenden Daten, so dass sicher gestellt sein muss, dass die Daten vom Typ A fertig geladen sind, bevor das Laden der Daten vom Typ B startet bzw. starten kann.
  2. Der Fortschritt des Ladevorgangs soll visualisiert werden, heißt, ist ein Ladevorgang abgeschlossen und es beginnt der nächste, soll ein Ladebalken vorwärts schreiten.

Es wird ersichtlich, dass es nicht damit getan ist, die AJAX-Calls hintereinander aufzurufen. Vielmehr braucht es eine ordnende Instanz, die dafür sorgt, dass die Aufrufe sauber von einander getrennt ablaufen und zwischendurch Callback-Funktionen aufgerufen werden können. Um das zu erreichen, greife ich auf das Pub-Sub-Pattern von Addy Osmani zurück und kombiniere es mit einer InitialDataloader-Funktion. Die AJAX-Funktionalitäten hole ich mir von jQuery.

Im folgenden das Pub-Sub-Pattern:

JavaScript

var mediator = (function () {

	function subscribe(channel, fn) {
		
		if ( !mediator.channels[channel] ) {
			mediator.channels[channel] = [];
		}

		mediator.channels[channel].push({
			context : this,
			callback : fn
		});

		return this;

	}

	function publish(channel, context) {

		var i = 0,
			len,
			args,
			subscription;

		if ( !mediator.channels[channel] ) {
			return false;
		}

		args = Array.prototype.slice.call(arguments, 2);
		len = mediator.channels[channel].length;

		for ( ; i<len; i += 1 ) {
			subscription = mediator.channels[channel][i];
			subscription.callback.apply((context || subscription.context), args);
		}

		return this;

	}

	return {
		channels : {},
		publish : publish,
		subscribe : subscribe,
		installTo : function (obj) {
			obj.subscribe = subscribe;
			obj.publish = publish;
		}
	}

})();

Auf das Pub-Sub-Pattern möchte ich gar nicht weiter eingehen. Lediglich der Hinweis, dass ich die Publish-Funktion um die Möglichkeit ergänzt habe, einen eigenen Kontext zu übergeben.

Kommen wir als nächstes zur InitialDataloader-Funktion:

JavaScript

var InitialDataloader = function (args) {					
	this.args = args;
	this.result = [];
	
	this.init();
};

InitialDataloader.prototype.init = function () {
	
	var i = 0,
		len = this.args.length;
	
	if ( !len ) {
		return;
	}
	
	for ( ; i<len; i+=1 ) {
		mediator.subscribe(this.args[i], this.load);
	}
	
	mediator.publish(this.args[0], this, {path : this.args[0], next : 1});

};

InitialDataloader.prototype.load = function (obj) {
	
	var that = this,
		deferred = $.ajax({url : obj.path});
	
	deferred.success(function (resp) {
		that.result.push('Data' + obj.next + ' loaded');
	});
		
	deferred.then( function (resp) {						
		if ( obj.next === that.args.length ) {
			console.log(that.result);
		} else {
			mediator.publish(that.args[obj.next], that, {path : that.args[obj.next], next : obj.next+1});
		}
	});
	
};

in der Konstruktor-Funktion werden die Argumente entgegen genommen und der Prozess via der init-Funktion gestartet. Das args-Argument stellt dabei einen Array dar, der die Pfade zu den Daten enthält.

In der init-Funktion wiederum wird für jeden Pfad des args-Array eine Funktion im Pub-Sub-Pattern angemeldet. Anschließend wird das erste Element des Pub-Sub-Patterns “veröffentlicht”, sprich die load-Funktion mit dem ersten Pfad aufgerufen und der Zähler zum nächsten Element des Pfad-Arrays mit übergeben.

Innerhalb der load-Funktion wird ein AJAX-Call auf den übergebenen Pfad gemacht und an ein deferred-Objekt übergeben. Dies wird anschließend genutzt, um eine Callback-Funktion zu starten, wenn der Ladevorgang abgeschlossen ist, und anschließend entweder das nächste Element des Pfad-Array im Pub-Sub-System zu “veröffentlichen” oder – wenn alle AJAX-Calls gemacht sind – eine finale Funktion aufzurufen (console.log(that.result);).

Anschließend noch der Aufruf der Funktion:

JavaScript

var initialDataLoad = new InitialDataloader([
	'http://apple.com/',
	'https://twitter.com/',
	'http://www.spiegel.de/',
	'http://www.flickr.com/'
]);

Die Adressen sind willkürlich gewählt.

Wenn man den Code in ein Skript packt und im Browser laufen lässt, sieht man in der Konsole nacheinander die AJAX-Calls und abschließend die Ausgabe des this.result-Array, der die einzelnen, zu ladenden Elemente mitgezählt hat.

Das war es auch schon. Sollte es Fragen oder Anregungen, nutzt bitte die Kommentar-Funktion. Ich freue mich immer über Feedback!

"Backboned" · AJAX-powered WordPress-Theme · Screenshot

Check out Backboned v2!!

Um mir gelegentlich etwas Zerstreuung vom Lernen zu geben, habe ich einen lange gehegten Plan in die Tat umgesetzt: Ein AJAX-betriebenes WordPress-Theme mit Backbone.js zu bauen. Und zwar keine auf “Graceful Degradation” setzende Kompromisslösung. Alle Inhalte werden asynchron geladen und sind per Hashbang URIs ansteuerbar. Ein Blick in den Quellcode offenbart, was ich meine: Ein JSON-Objekt mit allen grundlegenden Daten, eine Handvoll jQuery-Templates und das HTML-Grundgerüst. Das war es an statischem Content - die Darstellung des Inhalts geschieht über Backbone.js.

Damit Suchmaschinen nicht außen vor bleiben und man sich nicht die Mühe machen muss, einen “Headless Browser” à la HtmlUnit auf seinem Server zum laufen bringen zu müssen, werden grundsätzlich alle Inhalte als GET-Anfrage mit dem Parameter “_escaped_fragment_” abgehandelt - die Ausgabe variiert dann je nachdem zwischen statischem HTML oder einem nackten JSON-Objekt. So ist sichergestellt, dass die Inhalte trotzdem indiziert werden können. Die einzigen, die in die Röhre schauen, sind Besucher ohne JavaScript.

Um diesen Workaround zu realisieren, war jedoch ein hohes Maß an Improvisation vonnöten. Mit WordPress-Bordmitteln habe ich es nicht geschafft, das Frontend-seitige URL-Routing von Backbone.js server-seitig abzubilden und entsprechend zu bearbeiten. Ich habe deshalb auf ein simples MVC-Pattern zurück gegriffen und in das eigentliche Theme eine Art Child-Theme integriert. Das ist insgesamt kein Ansatz der mir - besonders in meiner Umsetzung - gefällt. Des weiteren muss man für einen störungsfreien Betrieb des Themes das URL-Rewriting in den WordPress-Einstellungen deaktivieren.

Darüber hinaus bleibt anzumerken, dass das Theme insgesamt eher rudimentär ist. Ich würde von einem Produktiveinsatz abraten. Allerdings bin ich grundsätzlich von der Idee des Themes überzeugt und freue mich natürlich, wenn jemand sich ebenfalls dafür begeistern kann und daran weiterarbeitet. Gerne auch in Kollaboration mit mir. Zu tun gibt es unter anderem noch:

  1. die grundsätzliche Verbesserung des PHP-Codes (sicherer machen, besser in das WordPress-Environment integrieren,…)
  2. den Funktionsumfang erhöhen (Neueste Kommentare, Tags, Suchfunktion, Sidebar-Widgets(?),…)
  3. das JavaScript straffen (Performance, geschmeidigere GUI-Abläufe,…)

…um ein paar Aspekte zu nennen.

Ansonsten freue ich mich wie immer über Anregungen und Verbesserungsvorschläge - gerade bei einem Vorhaben dieser Größenordnung hat man als Entwickler nicht wirklich einen umfassenden Überblick.

PS: Wahrscheinlich werde ich das Teil zeitnah bei GitHub reinladen. Muss mich da aber erst noch anschlauen.

Update: Okay, die Geschichte ist jetzt auch auf GitHub - https://github.com/herschel666/Backboned. Viel Spaß.

Backbone.js ist ein interessantes JavaScript-MVC, mit welchem ich mich seit etwa zwei Wochen beschäftige. Und nun möchte ich ein kleines Tutorial dazu präsentieren - wir bauen uns ein Merkliste. Das Konzept sieht wie folgt aus: Ständig laufen einem tolle Filme, Bücher und Spiele über den Weg, die man unbedingt noch sehen/lesen/spielen möchte, aber man merkt sie sich nie. Das ist der Punkt, wo die Merkliste ins Spiel kommt.

Bei der Programmierung habe ich mich stark an der Todo List Application orientiert, allerdings ist die Merkliste vom Funktionsumfang her schmaler, beinhaltet dafür aber einen Controller für das URL-Routing.

Aber nun zur Sache:

Als erstes benötigen wir den HTML-Teil.

HTML

<!doctype html>
<html lang="de">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	
	<title>Backbone.js-Tutorial - Merkliste</title>	
	<link rel="stylesheet" href="style.css" />
	
</head>

<body>

	<ul id="nav"></ul>
	<input type="text" placeholder="Gib einen Titel ein&hellip;" id="list_input" />
	<ul id="list"></ul>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
	<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.6/underscore-min.js"></script>
	<script src="http://ajax.cdnjs.com/ajax/libs/backbone.js/0.3.3/backbone-min.js"></script>
	<script src="js/backbone-localstorage.js"></script>
	<script src="js/list.min.js"></script>
	
	<script type="text/template" id="list-item-template">
		<strong><%= title %></strong>
		<span class="delete_item">x</span>
	</script>
	
	<script type="text/template" id="nav-template">
		<a href="#/category/<%= title %>"><%= title %></a>
	</script>
	
</body>
</html>

Für die Merklisten-App benötigen wir jQuery, Underscore.js, Backbone.js und die Backbone-Erweiterung Local-Storage, damit die Einträge im Browser gespeichert werden können. Des weiteren werden die Container für die Navigation und die Listeneinträge, sowie das Eingabefeld angelegt. Schlussendlich brauchen wir noch zwei Templates - eins für die Navigation, eins für die Liste.

Kommen wir nun zum JavaScript-Teil. Als erstes benötigen wir hier unser Model:

JavaScript

window.List = Backbone.Model.extend();

Als nächstes erstellen wir zwei Collections, eine für die Navigation, eine für die Listeneinträge:

JavaScript

window.NavCollection = Backbone.Collection.extend({
	model : List
});
	
window.ListCollection = Backbone.Collection.extend({				
		
	model : List,				
		
	localStorage : new Store('List'),
		
	getByCategory : function ( category )
	{
		return this.filter( function (item)
		{
			return item.get('category') == category;
		});
	}
});

In der Collection für die Listen-Einträge wird der Local Storage angemeldet und eine Funktion eingefügt, die es ermöglicht, die Einträge der Collection nach ihrer Kategorie zu filtern.

Kommen wir nun zum Controller der App:

JavaScript

window.ListController = Backbone.Controller.extend({
		
	_navModel : new NavCollection([
		{title : 'Filme'},
		{title : 'Buecher'},
		{title : 'Spiele'}
	]),
	_navViews : [],
	_categoryModel : new ListCollection,				
	_inputView : null,
		
	routes : {
		'' : 'init',
		'/category/:category' : 'getItems',
	},
		
	initialize : function ()
	{			
		this._navModel.each( function ( item, i )
		{
			this._navViews[i] = new NavigationView({
				model : item
			});
		}, this);
			
		Backbone.history.start();
	},
		
	init : function ()
	{
		window.location.hash = '/category/Filme';
	},
	
	getItems : function ( category )
	{			
		for ( view in this._navViews )
		{
			this._navViews[view]
				.render()
				.setClass();
		};
							
		if ( this._inputView == null )
		{		
			this._inputView = new ListInputView({
				model : this._categoryModel,
				category : category
			});
		}
		else
		{
			this._inputView.options.category = category;
			this._inputView.model.trigger('refresh');
		}
	}
		
});

Als erstes werden - jeweils für die Navigation und die Liste - neue Instanzen der zugehörigen Collection erstellt und Platzhalter für die jeweiligen Views angemeldet. Danach werden die relevanten Pfade mit Funktionen verknüpft. In diesem Fall wird die Funktion init() ausgeführt, wenn kein Hash vorhanden ist, und die Funktion getItems(), wenn ein Kategorie-Hash vorhanden ist.

Als nächstes folgt die initialize-Funktion, welche als erstes beim Aufrufen des Controllers ausgeführt wird. Dabei wird das Navigation-Model mit den nötigen Einträgen versehen, danach für jeden Eintrag der Navigation-Collection eine View-Instanz erstellt und im _navView-Array gespeichert, sowie die Backbone.history-Funktion gestartet.

Als nächstes wird die init-Funktion definiert. Diese sorgt einfach nur dafür, dass die Kategorie "Filme" gewählt wird, indem der entsprechende Kategorie-Hash gesetzt wird. Die Kategorie ist hierbei willkürlich von mir gewählt.

Danach folgt die Definition der getItems-Funktion. In dieser werden als erstes die Views für die Navigationspunkte ge-rendert. Daraufhin folgt entweder die Initialisierung des Views für die Listeneinträge, oder - falls dies schon geschehen ist - das Überschreiben der aktuellen, dem View übergebenen Kategorie und das Neu-Aufbauen der Liste mit den entsprechenden Einträgen. Dabei kommt die Filter-Funktion der Listen-Collection zum Einsatz.

Nachdem nun Model, Collection und Controller vorhanden sind, geht es an die Views. Davon benötigen wir drei - einen für das Eingabefeld, einen für die Liste und einen für die Navigation. Fangen wir mit dem Eingabefeld an:

JavaScript

window.ListInputView = Backbone.View.extend({
	
	el : $('#list_input'),
	
	list : $('#list'),
	
	events : {
		'keypress' : 'createListItem'
	},
	
	initialize : function ()
	{
		_.bindAll(this, 'addListItem', 'addAllListItems');
		
		this.model.bind('add', this.addListItem);
		this.model.bind('refresh', this.addAllListItems);
		
		this.model.fetch();
	},
	
	createListItem : function (e)
	{
		if ( e.keyCode == 13 )
		{			
			this.model.create({
				category : this.options.category,
				title : this.el.val()
			});
			
			this.el.val('');
			this.el.blur();
		}
	},
	
	addListItem : function ( item )
	{					
		var view = new ListItemView({model : item});
		
		!view.model.length && this.list.append( view.render().el );
	},
	
	addAllListItems : function ()
	{
		this.list.empty();
		
		_.each(this.model.getByCategory(this.options.category), function(item)
		{
			this.addListItem(item);
		}, this);
	}
	
});

Der ListInputView nimmt die Eingabe entgegen, erstellt eine neue Instanz des Listen-Eintrag-View und fügt füllt die Liste mit Einträgen. Die Funktion createListItem() erstellt einen neuen Eintrag in der Collection, wenn das Eingabefeld abgefeuert wurde. Über die Angabe this.model.bind('add', this.addListItem); in der initialize-Funktion wird gesorgt, dass daraufhin die Funktion addListItem() aufgerufen wird, die für den neuen Eintrag in der Collection eine Instant des Listen-Eintrag-View erstellt und diesem den Befehl render() mit auf den Weg gibt. Über die Funktion addAllListItems() wird einerseits sichergestellt, dass die Liste geleert wird, bevor neue Einträge nach einem Kategorie-Wechsel reingladen werden, und andererseits, bei Übergabe einer Collection mit mehreren einträgen, für jeden Eintrag die Funktion addListItem() ausgeführt wird.

Als nächstes kommen wir zum Listen-Eintrag-View:

JavaScript

window.ListItemView = Backbone.View.extend({
		
	tagName : 'li',
		
	className : 'list_item',
		
	tmpl : _.template( $('#list-item-template').html() ),
		
	events : {
		'click .delete_item' : 'removeItem'
	},
		
	render : function ()
	{		
		$(this.el).html( this.tmpl( this.model.toJSON() ));
			
		return this;
	},
		
	removeItem : function ()
	{
		this.model.destroy();
		$(this.el).fadeOut( function()
		{
			$(this).remove();
		});
	}
		
});

Hier wird das Element vom Standard (div) auf li gesetzt, die gewünschte CSS-Klasse gesetzt, das Template für den Eintrag angemeldet, ein Klick-Event mit der Funktion removeItem() verknüpft und anschließend die Funktionen render() und removeItem() definiert. Erstere fügt die Daten des Collection-Eintrags in das Template ein, letztere löscht den View und den dazugehörigen Collection-Eintrag.

Schlussendlich benötigen wir noch einen View für die Navigation:

JavaScript

window.NavigationView = Backbone.View.extend({
		
	tagName : 'li',
		
	tmpl : _.template( $('#nav-template').html() ),
		
	hash : function ()
	{
		return window.location.hash.replace('#/category/', '');
	},
		
	render : function ()
	{							
		$('#nav').append($(this.el).html( this.tmpl( this.model.toJSON() ) ));
			
		return this;
	},
		
	setClass : function ()
	{
		var curHash = this.hash();
			
		this.el.className = ( curHash == $(this.el).find('a').text() ) ? 'current' : '';
	}
		
});

Neu und interessant ist hier die Funktion setClass(). Diese sorgt nach dem Rendern der Navigation, dass der aktuelle Reiter die Klasse "current" bekommt.

Um die Listen-App nun zum Laufen zu bringen, erstellen wir eine neue Instanz des Controllers:

JavaScript

var listApp = new ListController();

Der ganze Spaß wird in eine anonyme jQuery-Funktion geschrieben, damit die App gestartet wird, sobald das DOM geladen ist.

Das war es auch schon. Ich hoffe, meine Erklärungen sind einigermaßen nachvollziehbar. Falls nicht, nutzt auf jeden Fall die Kommentar-Funktion. Des weiteren würde ich mich natürlich über Anregungen und Verbesserungsvorschläge freuen, da ich ja doch ein Neuling in Sachen Backbone.js bin. Ansonsten noch der Hinweis, dass bei Gefallen natürlich gerne regen Gebrauch von den unten stehenden Social-Media-Buttons gemacht werden kann.

Vielen Dank!

Heute möchte ich kurz zeigen, wie man mit etwas HTML, CSS und ein paar kleinen GIF-Grafiken eine pixelige Seiten-Navigation im 8Bit-Stil baut. Außerdem benutzen wir etwas jQuery-Magic, um der Navigation noch Fly-Out-Menus zu spendieren.

8Bit-Style-Navigation mit Fly-Out-Menus

Beginnen wir wie gewohnt mit dem HTML-Teil:

HTML
<div id="nav">
  <ul>
    <li class="top">
      <a href="index.html">
        <strong>Home</strong>
      </a>
      <div class="sub">
        <div>
          <ul>
            <li>
              <a href="#">Sub-Item 1</a>
            </li>
            <li>
              <a href="#">Sub-Item 2</a>
            </li>
            <li>
              <a href="#">Sub-Item 3</a>
            </li>
            <li>
              <a href="#">Sub-Item 4</a>
            </li>
            <li>
              <a href="#">Sub-Item 5</a>
            </li>
          </ul>
        </div>
      </div>
    </li>
    <li class="top">
      <a href="#">
        <strong>About</strong>
      </a>
      <div class="sub">
        <div>
          <ul>
            <li>
              <a href="#">One Sub-Item</a>
            </li>
            <li>
              <a href="#">Another Sub-Item</a>
            </li>
            <li>
              <a href="#">Still a Sub-Item</a>
            </li>
          </ul>
        </div>
      </div>
    </li>
    <li>
      <a href="#">
        <strong>Contact</strong>
      </a>
    </li>
  </ul>
</div>

Wie gewohnt eine ungeordnete Liste für die Haupt-Navigation und jeweils eine für die Sub-Navigationen. Um die charakteristischen Ecken hinzubekommen, müssen zwei Elemente ineinander verschachtelt und gegeneinander verschoben werden. Das macht den Quelltext Tag-intensiver. In meinen Augen jedoch noch in einem vertretbaren Rahmen und weit entfernt von klassischer "Diveritis".

Als nächstes kommen wir zum CSS:

CSS
@font-face {
  font-family: 'SilkscreenNormal';
  src: url('slkscr-webfont.eot');
  src: local('☺'), url('slkscr-webfont.woff') format('woff'), url('slkscr-webfont.ttf') format('truetype'), url('slkscr-webfont.svg#webfontUx1SMfhe') format('svg');
  font-weight: normal;
  font-style: normal;
}

ul {
  list-style: none;
}

body {
  background-color: #FFF;
  color: #333;
  font: normal 13px SilkscreenNormal, sans-serif;
}

#nav,
#nav > ul,
#nav > ul > li {
  float: left;
  display: inline;
}

#nav,
#nav > ul {
  width: auto;
  _width: 1%; /* IE6 Hack */
  height: 32px;
}

#nav {
  margin: 50px;
  position: relative;
  border-width: 2px 0;
  border-style: solid;
  border-color: #666;
}

#nav > ul {
  position: relative;
  left: -2px;
  margin-right: -4px;
  padding: 0 10px;
  border-width: 0 2px;
  border-style: solid;
  border-color: #666;
  background: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP///73n/yH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=') 0 0 repeat;
  *background: url('img/tile1.gif') 0 0 repeat; /* IE6 and IE7 can't handle data uris */
}
		
#nav > ul > li {
  _width: 1%; /* IE6 Hack */
  margin: 3px 5px;
  position: relative;
}
			
#nav > ul > li > a {
  display: block;
  position: relative;
  width: auto;
  height: 22px;
  border-width: 2px 0;
  border-style: solid;
  border-color: #999;
  background-color: #FFF;
}
				
#nav > ul > li > a:link,
#nav > ul > li > a:visited {
  color: #999;
  text-decoration: none;
  background: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP/////fvSH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=') 0 0 repeat;
  *background: url('img/tile2.gif') 0 0 repeat; /* IE6 and IE7 can't handle data uris */
}
				
#nav > ul > li > a:hover,
#nav > ul > li > a:focus,
#nav > ul > li > a:active {
  color: #666;
  background-image: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP/////MmSH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=');
  *background-image: url('img/tile3.gif'); /* IE6 and IE7 can't handle data uris */
				}
				
#nav > ul > li > a strong {
  display: block;
  position: relative;
  width: auto;
  height: 22px;
  padding: 0 10px;
  line-height: 22px;
  left: -2px;
  margin-right: -4px;
  border-width: 0 2px;
  border-style: solid;
  border-color: #999;
}
					
#nav > ul > li.top > a > strong {
  padding-left: 21px;
  background: url('data:image/gif;base64,R0lGODlhBgAEAIABAJmZmf///yH5BAEAAAEALAAAAAAGAAQAAAIHhI8WocuwCgA7') 5px center no-repeat;
  *background: url('img/arrow.gif') 5px center no-repeat; /* IE6 and IE7 can't handle data uris */
}
					
#nav > ul > li > a:hover,
#nav > ul > li > a:hover strong,
#nav > ul > li > a:focus,
#nav > ul > li > a:focus strong {
  border-color: #666;
}
				
#nav > ul > li.top > a:hover strong,
#nav > ul > li.top > a:focus strong {
  background-image: url('data:image/gif;base64,R0lGODlhBgAEAIABAGZmZv///yH5BAEAAAEALAAAAAAGAAQAAAIHhI8WocuwCgA7');
  *background-image: url('img/arrow_hv.gif'); /* IE6 and IE7 can't handle data uris */
}
				
#nav > ul > li > a:active {
  top: 1px;
}
				
.sub {
  position: absolute;
  width: auto;
  top: 24px;
  left: 0;
  padding-top: 9px;
  display: none;
}
				
.sub div {
  position: relative;
  border-top: 2px solid #666;
  border-bottom: 2px solid #666;
}
					
.sub div ul {
  position: relative;
  left: -2px;
  margin-right: -4px;
  border-left: 2px solid #666;
  border-right: 2px solid #666;
  background: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP///+7u7iH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=') 0 0 repeat;
  *background: url('img/tile4.gif') 0 0 repeat; /* IE6 and IE7 can't handle data uris */
}
						
.sub div ul li {
 border-top: 2px solid #666;
}
							
.sub div ul li:first-child {
  border-top: none;
}
						
.sub div ul li a {
  display: block;
  padding: 0 10px;
  line-height: 22px;
  font-size: 12px;
  white-space: nowrap;
}
								
.sub div ul li a:link,
.sub div ul li a:visited {
  color: #666;
}
								
.sub div ul li a:hover,´
.sub div ul li a:focus {
  background: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP///93d3SH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=') 0 0 repeat;
  *background: url('img/tile5.gif') 0 0 repeat; /* IE6 and IE7 can't handle data uris */
}
								
.sub div ul li a:active {
  background: url('data:image/gif;base64,R0lGODlhBAAEAIAAAP///8zMzCH5BAAAAAAALAAAAAAEAAQAAAIGTACGqBkFADs=') 0 0 repeat;
  *background: url('img/tile6.gif') 0 0 repeat; /* IE6 and IE7 can't handle data uris */
}

Als erstes binde ich den Pixel-Font "SilkScreen von Jason Kottke ein. Danach folgen die Angaben für die Navigation.

Interessant ist dabei, dass selbst das äußere Element <div id="nav"> die Angabe float: left hat, damit es sich der Breite des Inhalts anpasst. Das kann zu Layout-Problemen führen, weshalb man im praktischen Einsatz darauf achten muss, das Element direkt unterhalb der Navigation mit einem clear: left zu versehen.

Des weiteren kann man sehen, wie die charakteristischen Ecken zustande kommen: Das äußere Element hat jeweils unten und oben eine zwei-Pixel-starke border und das innere jeweils links und rechts. Das innere Element wird dann per left: -2px und margin-righ: -4px um jeweils zwei Pixel nach links und rechts aus dem umgebenden Element hinaus gezogen. Schon ist der gewünschte Effekt da.

Ebenfalls erwähnenswert sind die Grafiken. Da diese nur 4x4 Pixel bzw. 4x6 Pixel groß sind, lohnt es sich, sie in Form von Data URIs einzubinden und so unnötige HTTP Requests zu sparen. Blöderweise können IE6 und IE7 damit nicht umgehen, weshalb die richtigen Grafiken ebenfalls eingebunden werden müssen. Diese werden dann mithilfe des Star-Hack den beiden Browsern zugewiesen.

Bilder in Data URIs umwandeln könnt ihr übrigens mit diesem Online-Tool.

Zum Schluss noch etwas jQuery um die Fly-Out-Menu-Funktionalität zu realisieren:

JavaScript
$(function () {
  $('#nav li.top').hover( function() {
    $(this).find('div').stop(true, true).fadeIn('slow');
  }, function() {
    $(this).find('div').stop(true, true).fadeOut('slow');
  });
});

Ich denke, das ist die spartanischste Lösung und bedarf keiner weiteren Erläuterung.

Das war es auch schon. Die Navigation ist beliebig per Copy&Paste erweiterbar, denkt nur daran, den li-Elementen, die eine Sub-Navigation beinhalten, die Klasse top zu verpassen, damit die Fly-Out-Menu-Funktionalität gewährleistet ist.

Ansonsten wünsche ich viel Spaß mit der Navigation. Bei Fragen bitte wie immer die Kommentar-Funktion nutzen. Und bei Gefallen fleißig via Twitter und Facebook verbreiten. Vielen Dank :-)