Mit jQuery alten Browsern HTML5 beibringen
HTML5 ist voll von neuen Elementen, mit denen man moderne, benutzerfreundliche Webseiten erstellen kann.
Eines dieser Dinge ist ein neues Attribut für Eingabefelder, der placeholder
. Dabei handelt es sich um einen Text, der in dem Eingabefeld angezeigt wird, wenn der Nutzer noch keine Angabe gemacht hat. Das soll ihm dabei helfen, eine sinnvolle Angabe zu machen. Manchmal wird es auch anstelle eines Labels verwendet. In Firefox sieht das z.B. so aus:
Das nötige Markup dafür ist <input type="text" placeholder="Benutzername">
.
Leider wird der Placeholder nicht in allen Browsern unterstützt, insbesondere im Internet Explorer bis einschließlich der aktuellen Version 9. Wenn wir nicht mit einer schlechteren Benutzungsfreundlichkeit in älteren Browsern leben wollen, müssen wir dort das Verhalten mit Hilfe von JavaScript nachrüsten.
Wenn man eine Webseite mit JavaScript anreichern will, ist jQuery meist das Tool der Wahl. Man kann einfach auf Elemente der Seite zugreifen und sie manipulieren, dabei werden viele Browserinkompatibilitäten vor dem Nutzer verborgen. In jQuery steckt die Erfahrung vieler Entwickler und die Unterstützung einer großen Community und deckt eine breite Palette von Funktionen ab.
jQuery wurde aber nicht nur dafür ausgelegt, einfach zu benutzen, sondern auch einfach zu erweitern zu sein. Es gibt eine eigene Seite mit einer Vielzahl an Plugins, unter anderem auch für das Placeholder-Attribut. Das soll uns aber nicht davon abhalten, unser eigenes jQuery-Placeholder-Plugin zu schreiben.
Das Grundgerüst einer Erweiterung sieht so aus:
(function ($) { $.fn.myPlugin = function () { // mein Code return this; }; }(jQuery));
Mit $.fn
können wir jQuery erweitern. Wir fügen diesem Objekt eine neue Methode myPlugin
hinzu, die wir ab sofort über $(selector).myPlugin()
aufrufen können. Wichtig ist, am Ende this
zurückzugeben. this
ist in der Plugin-Funktion das jQuery-Objekt, auf dem sie aufgerufen wurde. Dadurch, dass wir es zurückgeben, ermöglichen wir das Chaining von jQuery, wie z.B. $(".foo").myPlugin().slideUp();
Technisch gesehen ist $.fn
eine Referenz auf jQuery.prototype
, also auf das Objekt, das der Prototyp aller Objekte ist, die über jQuery(selector)
erzeugt werden. Mit jQuery.fn.myPlugin
erweitern wir also diesen Prototypen, weshalb die Plugin-Funktion auf allen jQuery-Objekten aufgerufen werden kann.
In Zeile 1 und 9 sehen wir ein interessantes Pattern; man nennt es Immediately Invoked Function Expression (IIFE), und es erlaubt uns, Code zu schreiben, ohne dass darin verwendete Variablen im globalen Objekt landen. Das ist auch deshalb gut, weil dadurch gleichbenannte Variablen in verschiedenen Plugins diese Plugins nicht durcheinander bringen. Wir erzeugen damit eine Funktion, die sofort aufgerufen wird. Dabei geben wir das jQuery-Objekt hinein, verwenden es innerhalb der Funktion aber unter dem Namen $
. Die Funktionen, die wir im weiteren Verlauf implementieren, sind in der IIFE gekapselt und belasten so nicht das globale Objekt.
Um das Verhalten des Placeholder-Attributs nachzurüsten, sollten wir damit anfangen, dieses Verhalten zu beschreiben. Das Placeholder-Attribut bewirkt drei Dinge:
- Wenn der Wert des Eingabefeldes leer ist, wird anfangs der Placeholder angezeigt. Wenn der Wert nicht leer ist, passiert initial nichts.
- Wenn der Nutzer das Eingabefeld fokussiert (z.B. durch Klick oder über die Tastatur) und der Placeholder angezeigt wird, wird er entfernt.
- Wenn das Eingabefeld den Fokus verliert und der Wert leer ist, wird der Placeholder angezeigt. Wenn der Wert nicht leer ist, passiert nichts.
Manche Browserhersteller variieren das Verhalten leicht, indem der Placeholder angezeigt wird, bis der Nutzer tatsächlich eigenen Text eingibt. Das ist aus Nutzersicht sehr sinnvoll, aber deutlich schwieriger zu implementieren. Darum wollen wir uns an dem oben beschriebenen Verhalten orientieren.
Wenn wir genau hinschauen, sehen wir, dass initial und beim Defokussieren die gleichen Dinge passieren. Wir können nun mit der Implementierung beginnen:
$.fn.placeholder = function () { // this bezieht sich auf ein jQuery-Objekt // alle Elemente initialisieren this.each(showPlaceholder); // Event-Handler registrieren this.on({ "focus" : hidePlaceholder, "blur" : showPlaceholder }); return this; };
Wir definieren also ein neues Plugin placeholder
, das wir z.B. mit $("input").placeholder()
aufrufen können. In einem jQuery-Plugin bezieht sich this
auf das jQuery-Objekt selbst, in unserem Fall also auf $("input")
. Auf diesem Objekt können wir nun alle Funktionen aufrufen, die jQuery so bietet. Zunächst rufen wir für jedes Element aus dem Objekt die Funktion showPlaceholder
auf (die wir noch definieren müssen). Dann fügen wir allen Elementen Event-Handler hinzu (mit der Syntax aus jQuery 1.7, wer ältere Versionen benutzt, muss this.bind()
statt this.on()
verwenden).
Die in dem Plugin verwendeten Methoden implementieren wir jetzt. Dabei müssen wir beachten, dass sich innerhalb eines Event-Handlers this
nun auf das DOM-Element selbst bezieht, nicht mehr auf ein jQuery-Objekt. Das ist innerhalb der Funktion, die wir dem Aufruf von each()
übergeben, genauso. Wenn es sinnvoll ist, können wir wieder über $(this)
ein jQuery-Objekt erzeugen, häufig ist das allerdings nicht nötig.
function showPlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === "") { this.value = this.getAttribute("placeholder"); } } function hidePlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === this.getAttribute("placeholder")) { this.value = ""; } }
Wenn wir nun diese Programmteile zusammenbringen, haben wir eine erste Version unseres Plugins fertig!
(function ($) { function showPlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === "") { this.value = this.getAttribute("placeholder"); } } function hidePlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === this.getAttribute("placeholder")) { this.value = ""; } } $.fn.placeholder = function () { // this bezieht sich auf ein jQuery-Objekt // alle Elemente initialisieren this.each(showPlaceholder); // Event-Handler registrieren this.on({ "focus" : hidePlaceholder, "blur" : showPlaceholder }); return this; }; }(jQuery));
Mehr als eine erste Version ist es aber noch nicht. Wir iterieren über alle Elemente, die in this
enthalten sind, und fügen ihnen das Placeholder-Verhalten zu, ohne zu beachten, ob das sinnvoll ist. Wir könnten ja das Plugin auch über $("span").placeholder()
aufrufen, was zwar vollkommen unsinnig wäre, aber von unserem Code nicht überprüft wird. Wenn wir ein Plugin nicht nur für den eigenen Gebrauch schreiben, wissen wir nicht, wer dieses Plugin verwendet. Darum müssen wir dafür sorgen, dass das Plugin so robust wie möglich ist. Das entspricht dem Prinzip der Robustheit der Softwareentwicklung (auch Postel’s Law genannt):
„Be conservative in what you do, be liberal in what you accept from others“ („sei zurückhaltend bei dem, was du tust, und offen bei dem, was du von anderen akzeptierst“)
Im Falle unseres Plugins bedeutet das, dass wir den Aufruf auf allen jQuery-Objekten erlauben (liberal), uns aber auf diese Elemente beschränken, die ein Placeholder-Attribut haben (conservative):
$.fn.placeholder = function () { var elements = this.filter("[placeholder]"); elements.each(showPlaceholder); elements.on({ "focus" : hidePlaceholder, "blur" : showPlaceholder }); return this; };
Ein weiteres Problem ergibt sich beim Absenden eines Formulars, das Eingabefelder mit Placeholder enthält. Weil wir ja den Placeholdertext als Wert des Feldes setzen, wird dieser Wert auch übertragen, obwohl der Nutzer keine Eingabe gemacht hat. Um das zu lösen, müssen wir diese Felder vor dem Versenden des Formulars wieder leeren.
function hideAllPlaceholders() { // this bezieht sich auf das Formular, das abgeschickt werden soll $(this).find("[placeholder]").each(hidePlaceholder); } $.fn.placeholder = function () { // ... elements.closest("form").on({ "submit" : hideAllPlaceholders }); };
Wir möchten vor dem Absenden des Formulars die eingefügten Placeholdertexte entfernen. Auf das Absenden können wir über den submit
-Event reagieren. Das zugehörige Formular finden wir über die jQuery-Methode closest()
, die ausgehend von einem Element das nächste Elternelement findet, auf das der angegebene Selektor passt. Dabei werden Duplikate nur ein mal berücksichtigt, das heißt, wenn mehrere Elemente des jQuery-Objekts dasselbe passende Elternelement haben, ist dieses nur einmal in dem Resultat von closest()
enthalten. Mit dem Code in den Zeilen 9-11 fügen wir dem Formular (oder den Formularen) einen Event-Handler zu. In diesem Event-Handler suchen wir alle Elemente mit Placeholder-Attribut, und leeren jedes Feld, wenn darin der Placeholdertext steht (Zeile 3). Das ist genau das, was in der bereits definierten Funktion hidePlaceholder
passiert, weshalb wir sie hier wiederverwenden.
Jetzt haben wir eine deutlich robustere Version. Ein großes Problem bleibt aber noch bestehen: Wir bauen die Funktionalität des Placeholder-Attributs auch für Browser nach, die es schon nativ unterstützen. Wir müssen also noch herausfinden, ob der Browser unser Plugin nötig hat, und uns überlegen, wie wir dieses Wissen verwenden.
Die Möglichkeit
if (!hasNativeSupport) { $.fn.placeholder = function () { // ... } }
scheidet aus, weil wir damit eine Inkonsistenz zwischen verschiedenen Browsern einführen würden. Unser Plugin $(selector).placeholder()
soll in allen Browsern aufgerufen werden können, aber nur für die Browser etwas tun, die es nötig haben. Wir sollten stattdessen in der Pluginfunktion entscheiden, ob wir etwas tun müssen.
Die Erkennung, ob ein Browser das Placeholder-Attribut unterstützt, erfolgt durch eine einfache Feature-Detection:
var hasNativeSupport = "placeholder" in document.createElement("input");
Wir erzeugen ein neues (temporäres und ungenutztes) input
-Element und überprüfen, ob es ein Property placeholder
hat. Der Wert interessiert uns dabei nicht; sofern es das Property gibt, unterstützt der Browser den Placeholder nativ.
Unsere Plugin-Funktion könnte also so aussehen:
$.fn.placeholder = function () { if (!hasNativeSupport) { // ... } return this; };
Nun ist es leider so, dass ein Browser, der das Placeholder-Attribut auf Eingabefeldern unterstützt, das nicht zwangsweise auch für Textareas tut. Opera vor Version 11.5 ist ein Beispiel. Darum müssen wir eine genauere Überprüfung vornehmen.
Die Idee dabei ist, herauszufinden, für welche Elemente der Browser den Placeholder nicht unterstützt, und diese Elemente durch Selektoren zu beschreiben. Diese Selektoren können wir dann der filter()
-Methode übergeben.
"textarea[placeholder]"
für Browser, die das Placeholder-Attribut nur aufinput
-Elementen unterstützen."input[placeholder]"
für Browser, die das Placeholder-Attribut nur auftextarea
-Elementen unterstützen."input[placeholder], textarea[placeholder]"
für Browser, die das Placeholder-Attribut nicht unterstützen.
Die gesammelten Selektoren bauen wir wie folgt zu einem einzelnen, kombinierten Selektor zusammen:
var selector = []; $.each(["input", "textarea"], function (index, nodeName) { if (!("placeholder" in document.createElement(nodeName))) { selector.push(nodeName + "[placeholder]"); } }); selector = selector.join(", ");
Wir erzeugen ein Array, in dem wir alle Selektoren, die der Browser benötigt, sammeln (Zeile 1). Dann lassen wir eine Schleife laufen über die Elementnamen input
und textarea
(Zeile 3), in der die Erkennung läuft (Zeile 4). Wenn der Browser das Placeholder-Attribut nicht unterstützt, fügen wir den Selektor input[placeholder]
bzw. textarea[placeholder]
dem Array hinzu (Zeile 5). Zum Schluss erzeugen wir aus dem Array einen String, indem wir die Elemente durch ein Komma separiert aneinanderhängen (Zeile 9).
Was passiert nun, wenn der Browser das Placeholder-Attribut vollständig unterstützt? Dann ist selector
ein Leerstring. Wir könnten nun if (!hasNativeSupport) {}
ersetzen durch if (selector) {}
, das ist aber nicht nötig. Wenn wir der filter()
-Methode einen Leerstring übergeben, bleibt kein Element übrig. Damit passiert in each()
und on()
nichts – was genau das ist, was wir brauchen.
Damit ist die zweite Version unseres Plugins fertig!
(function ($) { // Selektor erzeugen var selector = []; $.each(["input", "textarea"], function (index, nodeName) { if (!("placeholder" in document.createElement(nodeName))) { selector.push(nodeName + "[placeholder]"); } }); selector = selector.join(","); // Event-Handler function showPlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === "") { this.value = this.getAttribute("placeholder"); } } function hidePlaceholder() { // this bezieht sich auf ein DOM-Element if (this.value === this.getAttribute("placeholder")) { this.value = ""; } } function hideAllPlaceholders() { // this bezieht sich auf das Formular, das abgeschickt werden soll $(this).find(selector).each(hidePlaceholder); } // Plugin definieren $.fn.placeholder = function () { // this bezieht sich auf ein jQuery-Objekt var elements = this.filter(selector); // alle Elemente initialisieren elements.each(showPlaceholder); // Event-Handler registrieren elements.on({ "focus" : hidePlaceholder, "blur" : showPlaceholder }); elements.closest("form").on({ "submit" : hideAllPlaceholders }); return this; }; }(jQuery));
Wir verwenden das Plugin z.B. mit
$("input, textarea").placeholder();
Wir sind jetzt schon sehr weit gekommen und sollten uns endlich die Sinnfrage stellen. Brauchen wir eigentlich ein jQuery-Plugin? Könnten wir nicht den Code in unserer Funktion einmalig beim Seitenladen aufrufen?
var elements = $(selector); element.each(showPlaceholder); // ...Und die Antwort lautet, nein, wir brauchen nicht zwangsläufig ein jQuery-Plugin. Zumindest nicht, solange unsere Seite statisch ist und nach dem Laden keine neuen Elemente hinzukommen. Wenn wir aber eine dynamische Seite haben, bei der im Laufe der Zeit ein weiteres Eingabefeld hinzukommt, ist es praktisch, einfach
$(newInputField).placeholder()
aufrufen zu können.
Sind wir nun fertig? Wir haben das Plugin deutlich robuster gemacht, als Feinschliff fehlen aber noch einige Dinge:
- Der Placeholder wird üblicherweise in einer anderen Schriftfarbe angezeigt, z.B. grau statt schwarz.
- Wenn der Nutzer in das Eingabefeld genau den Placeholder-Text eingegeben hat, wird beim Fokussieren fälschlicherweise angenommen, das Feld müsse geleert werden. Zugegeben, das ist ein seltener Fall, aber nicht unmöglich. In dem Eingangsbeispiel wäre das der Fall, wenn der Benutzername „Benutzername“ wäre.
- Das Plugin funktioniert nicht für Passwortfelder.
- Wir leeren die Felder, die den Placeholder zeigen, beim Absenden des Formulars. Wenn das Absenden aber nicht zum Neuladen einer Seite führt (z.B. weil das Formular als
target
einen Iframe angegeben hat), fehlen die Placeholder danach.
Fangen wir damit an, den Placeholder stylebar zu machen. Wir könnten entweder in der showPlaceholder
-Methode die Schriftfarbe auf grau setzen (und in der hidePlaceholder
wieder zurücksetzen), oder wir können eine CSS-Klasse setzen, mit der der Nutzer des Plugins das Feld stylen kann. Mir persönlich gefällt der Ansatz mit der CSS-Klasse besser, weil in den meisten Browsern, die das Placeholder-Attribut unterstützen, auch die Farbe per CSS gesetzt werden kann. Wenn wir in unserem Plugin eine Klasse auf die Elemente setzen, in denen der Placeholder gerade angezeigt wird, kann das Styling einheitlich an einer Stelle vorgenommen werden:
/* schwarze Textfarbe für Eingabefelder */ input { color: black; } /* Placeholdertext in grau für Safari und Chrome */ input::-webkit-input-placeholder { color: gray; } /* ...für Firefox, */ input:-moz-placeholder { color: gray; } /* ...für Browser ohne native Unterstützung */ input[placeholder].empty { color: gray; }
CSS3 unterscheidet zwischen Pseudoelementen (z.B. before) und Pseudoklassen (z.B. hover). Pseudoklassen werden durch einen Doppelpunkt gekennzeichnet, Pseudoelemente durch zwei Doppelpunkte. Der Placeholder ist ein Pseudoelement, daher müsste es durch zwei Doppelpunkte gekennzeichnet werden (wie Chrome es schon umsetzt). Firefox hängt da noch zurück.
Wir müssen nun die Methoden showPlaceholder
und hidePlaceholder
anpassen. Da wir jetzt Klassennamen setzen wollen, ist es sinnvoll, ein jQuery-Objekt zu verwenden, statt auf das DOM-Element direkt zuzugreifen.
function showPlaceholder() { var element = $(this); if (element.val() === "") { element.val(element.attr("placeholder")); element.addClass("empty"); } } function hidePlaceholder() { var element = $(this); if (element.val() === element.attr("placeholder")) { element.val(""); element.removeClass("empty"); } }
Nun mag der Klassenname empty
nicht jedem gefallen, also können wir das Plugin so verändern, dass der Nutzer den Klassennamen frei angeben kann:
$.fn.placeholder = function (emptyClassName) { var elements = this.filter(selector); // Den übergebenen Klassennamen setzen oder "empty", wenn keiner übergeben // wurde elements.data("emptyClassName", emptyClassName || "empty"); // ...Initialisierung wie vorher return this; };
Über elements.data()
können wir Daten an einzelnen Elementen speichern. Wir übergeben dabei einen Schlüssel, über den wir die Daten später wieder abrufen können, und den Wert.
In den Methoden showPlaceholder
und hidePlaceholder
müssen wir dann noch kleine Änderungen vornehmen.
element.addClass(element.data("emptyClassName"));
und analog bei removeClass
.
Das zweite Problem können wir nun auch angehen. Durch den Klassennamen, den wir soeben hinzugefügt haben, können wir unterscheiden zwischen dem Fall, dass der Nutzer den Placeholdertext eingegeben hat (der Wert entspricht zwar dem Placeholdertext, aber der Klassenname fehlt) und dem, dass der Nutzer das Feld leergelassen hat (der Wert entspricht dem Placeholdertext, und der Klassenname ist gesetzt). Wir müssen die Methode hidePlaceholder
anpassen:
function hidePlaceholder() { var element = $(this), emptyClassName = element.data("emptyClassName"); if (element.val() === element.attr("placeholder") && element.hasClass(emptyClassName)) { element.val(""); element.removeClass(emptyClassName); } }
Das Plugin funktioniert nicht für Passwortfelder, weil der Wert in solchen Feldern nicht angezeigt wird. Natürlich kann man dieses Problem auch lösen (empfehlenswert ist das Plugin von Mathias Bynens), wir belassen es an dieser Stelle bei diesem kleinen Schönheitsfehler. Um das Plugin robust zu halten, können wir Passwortfelder durch var elements = this.filter(selector).not(":password");
explizit ausnehmen.
Bleibt noch das letzte Problem. Wie können wir die Placeholder, die wir vor dem Absenden des Formulars entfernt haben, danach wieder anzeigen? Wenn ein Formular abgesendet wird, passieren zwei Dinge hintereinander:
- Die Eventhandler für das Submit-Event werden aufgerufen.
- Das Formular wird abgeschickt.
Diese beiden Dinge erfolgen in einem Block. Leider gibt es kein afterSubmit
-Event, der danach aufgerufen würde. Wir können aber im Eventhandler über einen kleinen Trick dafür sorgen, dass nach dem Versenden Code ausgeführt wird. Dazu benötigen wir einen Timeout über window.setTimeout(callback, 0)
. Mit der Verzögerung von 0ms weisen wir den Browser an, den Callback auszuführen, sobald er mit dem fertig ist, was vorher schon anstand – in unserem Fall das Absenden des Formulars (wie Timeouts im Detail funktionieren, kann man gut in einem Artikel von John Resig nachlesen).
function hideAllPlaceholders() { var elements = $(this).find(selector); elements.each(hidePlaceholder); window.setTimeout(function () { elements.each(showPlaceholder); }, 0); }
Eine Kleinigkeit können wir zum Abschluss noch verbessern. Wir verwenden an mehreren Stellen die Strings "placeholder"
und "emptyClassName"
. Beim Schreiben dieses Plugins habe ich mich zweimal vertippt. Wenn wir diese Strings in Variablen herausziehen, reduzieren wir die Fehleranfälligkeit, haben nur eine Stelle, die wir anfassen müssen, wenn wir die Werte ändern wollen, und ermöglichen Code-Kompressoren (wie dem YUI-Compressor oder dem Google Closure Compiler) eine höhere Effizienz.
(function ($) { var placeholderAttr = "placeholder", placeholderSelector = "[" + placeholderAttr + "]", emptyClassNameAttr = "emptyClassName", defaultEmptyClassName = "empty", selector = []; // Erzeugen des Selektors $.each(["input", "textarea"], function (index, nodeName) { if (!("placeholder" in document.createElement(nodeName))) { selector.push(nodeName + placeholderSelector); } }); selector = selector.join(","); function showPlaceholder() { var element = $(this); if (element.val() === "") { element.val(element.attr(placeholderAttr)); element.addClass(element.data(emptyClassNameAttr)); } } function hidePlaceholder() { var element = $(this), emptyClassName = element.data(emptyClassNameAttr); if (element.val() === element.attr(placeholderAttr) && element.hasClass(emptyClassName)) { element.val(""); element.removeClass(emptyClassName); } } function hideAllPlaceholders() { var elements = $(this).find(selector); elements.each(hidePlaceholder); window.setTimeout(function () { elements.each(showPlaceholder); }, 0); } $.fn.placeholder = function (emptyClassName) { // this bezieht sich auf ein jQuery-Objekt var elements = this.filter(selector); // Den übergebenen Klassennamen setzen oder "empty", wenn keiner // übergeben wurde elements.data(emptyClassNameAttr, emptyClassName || defaultEmptyClassName); // alle Elemente initialisieren elements.each(showPlaceholder); // Event-Handler registrieren elements.on({ "focus" : hidePlaceholder, "blur" : showPlaceholder }); elements.closest("form").on({ "submit" : hideAllPlaceholders }); return this; }; }(jQuery));
Jetzt ist unser Plugin endgültig fertig. Wir haben gelernt, wie man ein jQuery-Plugin schreibt, haben das Verhalten des Placeholder-Attributs studiert, eine Feature-Detection geschrieben, um herauszufinden, ob der Browser das Plugin benötigt, und zum Abschluss Schwächen der Implementierung herausgestellt und behoben.