Das nachfolgende Tutorial soll Euch anhand eines kleinen Beispiels die Verwendung von Javascript als behaviour layer näherbringen. Der Übersicht halber verzeiht mir bitte den aufs wesentliche reduzierten HTML Code. Zum Nachvollziehen bietet sich Firefox mit der Firebugerweiterung an, da sie auch nachträglich geänderte Attributwerte anzeigt.
Empfohlene Grundlagen
Aufgabenstellung
Wir wollen als Beispiel ein einfaches Infofeld realisieren, das über ein Mausereignis an- und ausgeschaltet werden kann. Ein üblicher Ansatz wäre hier der folgende:
toggleDiv.css:
toggleDiv.js:
Das Schaltelement p ruft über ein onClick Ereignis die eigentliche Wechelfunktion auf. Dort wird auf das Infofeld über die ID zugegriffen und dann dessen CSS Eigenschaft 'display' geändert, was eine Sichtbar/Unsichtbarschaltung zur Folge hat. Der Übersicht halber wurde das An- und Ausschalten bereits in eine JS Funktion toggleDivArea ausgelagert. Die Schreibweise der Funktion weicht hier von dem Schema ab, das allgemein in JS (Einsteiger-)Lehrbüchern verwendet wird, wird uns aber später noch gute Dienste leisten.
Die gezeigte Lösung weist einige Schwächen auf, die wir nachfolgend sukzessive auflösen wollen:
Unser erklärtes Ziel
Was mit CSS schon lange gang und gäbe ist, hält für Javascript erst langsam Einzug: die Abstraktion als eigene Ebene, den sogenannten 'behaviour layer'. Wie mit CSS die Formateigenschaften sollen über Attribute wie class und id mit Javascript funktionelle Eigenschaften definiert werden können. Unser obiges Beispiel könnte dann so aussehen:
Der Unterschied erscheint zunächst subtil. Nehmen wir jedoch an, wir müßten die Daten in einer Datenbank speichern, so ist die Zuordnung Attribut-Wert bereits wesentlich einfacher geworden als die onClick Angabe im Ausgangsbeispiel. Noch ersichtlicher wird das ganze, wenn wir uns vorstellen, dass allein mit diesem Code auch mehrere Events eingerichtet werden könnten. Dazu aber später mehr.
Schritt 1: Eine Grundlage
Unser Ziel bietet schon mal die erste Hürde: JS stellt zwar die Funktionen getElementById, getElementsByName und getElementsByTagName bereit. Was wir jedoch gut gebrauchen könnten ist eine getElementsByClass Methode. Elements wohlgemerkt, denn das class Attribut kann und soll mehrfach vergeben werden. Und die bauen wir uns nun.
Die Funktion gibt eine Reihe von Referenzen auf Objekte zurück, die in einem Array gesammelt werden. Ausgangspunkt ist das document Objekt, dessen Unterobjekte in einer Schleife durchlaufen werden und dabei vermittels eines regulären Ausdrucks auf das Vorhandensein eines gegebenen Klassenattributwertes geprüft werden.
Die Funktion ist übrigens eine abgeänderte Version von 'The Ultimate getElementsByClassName' [1], ist aber auch mannigfaltig anderswo im Netz zu finden.
Schritt 2: Stil entwickeln
Wir verabschieden uns von oElement.style.display='block' und abstrahieren. Wir führen statt dessen in unser bisheriges Stylesheet eine neue Formatklasse ein, die fortan von JS geschaltet wird. Für unser Beispiel reicht eine 'hidden' Klasse:
toggleDiv.css:
Natürlich muß dabei für #Info das display:none; entfernt werden, sonst würde unsere neu gesetzte Klassenformatierung keine Wirkung zeigen.
An unserem HTML Code ändert sich nichts, JS muß jedoch angepaßt werden:
toggleDiv.js:
Wir führen eine neue Funktion modifyClass ein, die nichts weiter tut, als den Wert des class-Attributes eines via oElement referenzierten Objektes um einen String zu erweitern bzw. diesen wieder zu entfernen. Um die Funktion etwas praktischer zu gestalten, fügen wir noch den iStatus Parameter dazu, mit dem wir das Einfügen oder Entfernen über 1 bzw. 0 explizit setzen können. Ohne diese Angabe wird der Attributwert über einen regulären Ausdruck auf den entspr. String durchsucht und bei Nichtvorhandensein darum erweitert. Ist der String vorhanden, wird er dagegen aus der Attributangabe entfernt.
Anregungen zur Erweiterung:
Schritt 3: Simon befiehlt
Unser Grundgerüst steht. Nun geht es um die Layerfunktionalität des JS Codes. Bisher wurde die Aktion ja durch das onclick Attribut im HTML Code des p tags eingeleitet. Dazu muß man wissen, dass jedes dieser Events in JS auch so gesetzt werden kann: Objektbezug.Event = Funktion; also z.B.:
Die nächste Funktion entsteht:
Über den Aufruf von addToggleEvent kann ich einen Elementbezug übergeben. Diesem Element wird dann die Eigenschaft onclick zugeordnet. Ein Mausklick auf das entspr. Element löst dann wie gewohnt den Div Wechsel aus. Genauso könnte man hier weitere Events zuordnen, bspw. onmouseover um eine Klasseneigenschaft für hover zu implementieren oder dergleichen.
Da das noch nicht sehr komfortabel ist - schließlich muß ja vorher noch der Zugriff auf das Element erfolgen - erweitern wir unser JS um eine kleine Funktion, die diese Zuordnung einmalig für alle gewünschten Elemente übernimmt. Und hierbei kommt endlich unsere getElementsByClass Methode zum Einsatz. Wir werden unser Schalter-Element wahrscheinlich ohnehin später mit CSS formatieren, mit geeignetem Mauszeiger und einer Formatierung, die auf seine Funktion hinweist. Nichts liegt näher, als die dafür verwendete Klasse auch als Markierung für unsere JS Funktionalität einzusetzen. Es gilt also, alle Elemente mit der Klasseneigenschaft 'switcher' (siehe Ausgangsbeispiel) an die addToggleEvent Funktion zu übergeben. Und das geht so:
Thats it. Ein Aufruf von addToggleEvents erweitert unseren HTML Code um die gewünschte Wechsel-Funktionalität.
Anregungen zur Erweiterung:
Schritt 4: Viva Autonomia - Modularität
Noch stört der Scriptaufruf am Ende. Schöner wäre doch, toggleDiv.js oben einzubinden und dabei schon automatisch die Toggle Funktion einzurichten. Das Problem ist dabei, dass alle Elemente bereits vorhanden sein müssen, die die onClick Event-Eigenschaft zugeordnet bekommen. Wir benötigen also einen Anstoß, wenn alle Elemente geladen sind. Genau das ermöglicht das window.onLoad Event. Eine Zeile in unserem toggleDiv.js genügt dafür:
Toll oder? Und es geht noch besser.
Schritt 5: Keine Streitigkeiten am Tisch!
Die nächste und für dieses Tutorial letzte Frage wirft Drew McLellan in seinem Artikel Writing Responsible JavaScript[2] auf: JS als Layer - wunderbar, aber was passiert, wenn unser Konzept window.onload = addToggleEvents; auch in anderen JS 'Modulen' verfolgt wird? Jede Zuweisung a´la window.onload = ... wird die alte überschreiben und schließlich wird nur die Funktionalität des zuletzt eingebundenen JS Layers übrig bleiben.
Gottlob gibts auch dafür eine Lösung. Als - man könnte sagen - Quasistandard hat sich folgendes Fragment von Simon Willison[3] herausgebildet, das ich lediglich in meine Variablenschreibweise umgemünzt habe:
Kurz umrissen wird das zum Zeitpunkt gesetzte onLoad Ereignis ausgelesen. Ist es bisher undefiniert, wird es auf die neu gewünschte Funktion gesetzt, anderenfalls wird das Event einfach um die Funktion erweitert, indem die bisherige und die neu auszuführende Funktion in einem function Statement gekapselt und dem onLoad Event zugewiesen werden.
Das macht unser Modul komplett. Statt egoistisch window.onload = addToggleEvents; zu benutzen, geben wir auch anderen Modulen die Freiheit window.onload zu nutzen und ändern den Aufruf in addOnLoadEvent (addToggleEvents); ab.
Resultat (erweitert um einige Spirenzien)
toggleDiv.css:
toggleDiv.js:
Quellen
[1] http://www.robertnyman.com/2005/11/0...tsbyclassname/
[2] http://24ways.org/2006/writing-responsible-javascript
[3] http://simonwillison.net/2004/May/26/addLoadEvent/
Und jetzt seid ihr dran...
Empfohlene Grundlagen
- HTML
CSS
Javascript Syntax
JS Events + zugehörige HTML Attribute
Dynamisches HTML über DOM Zugriff
Aufgabenstellung
Wir wollen als Beispiel ein einfaches Infofeld realisieren, das über ein Mausereignis an- und ausgeschaltet werden kann. Ein üblicher Ansatz wäre hier der folgende:
toggleDiv.css:
Code:
.switcher {background-color:#bbb; margin:0; } #Info {display:none; background-color:#ff0; height:3em;width:20em; }
Code:
toggleDivArea = function (sID) { oElement = document.getElementById (sID); if (oElement.style.display=='none') oElement.style.display='block'; else oElement.style.display='none'; }
Code:
<html><head> <link rel="stylesheet" type="text/css" href="toggleDiv.css" /> <script type="text/javascript" src="toggleDiv.js"></script> </head><body> <p class="switcher" onclick="toggleDivArea ('Info');">+ Schalter</p> <div id="Info">Unser Infofeld</div> </body></html>
Die gezeigte Lösung weist einige Schwächen auf, die wir nachfolgend sukzessive auflösen wollen:
- 1. Die verwendete Style Eigenschaft 'display' ist fest verdrahtet. Wollen wir das Infofeld später bspw. verkleinern statt ausblenden muß der Code der Funktion geändert werden. Die Funktion ist nicht mehr mehrfach verwendbar.
2. Das onclick Ereignis bezieht sich auf eine feste ID, die bei Bedarf direkt im enthaltenen Javascript Code geändert werden muß.
3. Das onclick Ereignis ist durch ein Attribut festgelegt. Wollen wir mehrere Infofenster gleichzeitig ändern, muß überall der Parameter angepaßt werden.
Unser erklärtes Ziel
Was mit CSS schon lange gang und gäbe ist, hält für Javascript erst langsam Einzug: die Abstraktion als eigene Ebene, den sogenannten 'behaviour layer'. Wie mit CSS die Formateigenschaften sollen über Attribute wie class und id mit Javascript funktionelle Eigenschaften definiert werden können. Unser obiges Beispiel könnte dann so aussehen:
Code:
<html><head> <link rel="stylesheet" type="text/css" href="toggleDiv.css"> <script type="text/javascript" src="toggleDiv.js"></script> </head><body> <p class="switcher" tgg_target="Info">+ Schalter</p> <div id="Info">Unser Infofeld</div> </body></html>
Schritt 1: Eine Grundlage
Unser Ziel bietet schon mal die erste Hürde: JS stellt zwar die Funktionen getElementById, getElementsByName und getElementsByTagName bereit. Was wir jedoch gut gebrauchen könnten ist eine getElementsByClass Methode. Elements wohlgemerkt, denn das class Attribut kann und soll mehrfach vergeben werden. Und die bauen wir uns nun.
Code:
getElementsByClass = function (sClassName) { var aReturnElements = []; var oRegExpClass = new RegExp ('\\s*\\b' + sClassName + '\\b'); var oElements = (document.all) ? document.all : document.getElementsByTagName ('*'); for (var i = 0 ; i < oElements.length ; i++) { if (oRegExpClass.test (oElements[i].className)) aReturnElements.push (oElements[i]); } return (aReturnElements); }
Die Funktion ist übrigens eine abgeänderte Version von 'The Ultimate getElementsByClassName' [1], ist aber auch mannigfaltig anderswo im Netz zu finden.
Schritt 2: Stil entwickeln
Wir verabschieden uns von oElement.style.display='block' und abstrahieren. Wir führen statt dessen in unser bisheriges Stylesheet eine neue Formatklasse ein, die fortan von JS geschaltet wird. Für unser Beispiel reicht eine 'hidden' Klasse:
toggleDiv.css:
Code:
.switcher {background-color:#bbb; margin:0; } #Info {background-color:#ff0; height:3em;width:20em; } .hidden {display:none; }
An unserem HTML Code ändert sich nichts, JS muß jedoch angepaßt werden:
toggleDiv.js:
Code:
toggleDivArea = function (sID , iStatus) { oElement = document.getElementById (sID); modifyClass (oElement , 'hidden'); } modifyClass = function (oElement , sValue , iStatus) { var iStatus = iStatus || -1; var oRegExpClass = new RegExp ('\\s*\\b' + sValue + '\\b'); if (-1 == iStatus) iStatus = ! oRegExpClass.test (oElement.className); switch (iStatus) { case true : case 1 : case '1' : oElement.className += ' ' + sValue; break; default: oElement.className = oElement.className.replace (oRegExpClass , ''); break; } }
Anregungen zur Erweiterung:
- - modifyClass könnte einen weiteren String als Parameter erhalten, der anstelle des leeren Strings im default case eingesetzt wird.
- toggleDivArea könnte einen zweiten modifyClass Aufruf enthalten, der je nach Ausgangslage (also nach gesetzten class Value bei Scriptaufruf) mit oder entgegengesetzt dem 'hidden' Wert gesetzt und entfernt wird.
Schritt 3: Simon befiehlt
Unser Grundgerüst steht. Nun geht es um die Layerfunktionalität des JS Codes. Bisher wurde die Aktion ja durch das onclick Attribut im HTML Code des p tags eingeleitet. Dazu muß man wissen, dass jedes dieser Events in JS auch so gesetzt werden kann: Objektbezug.Event = Funktion; also z.B.:
Code:
oElement.onclick = toggleDivArea;
Code:
addToggleEvent = function (oElement) { var sTargetIdentifier = 'tgg_target'; var sTarget = oElement.getAttribute (sTargetIdentifier , false); oElement.removeAttribute (sTargetIdentifier , false); oElement.onclick = function () { toggleDivArea (sTarget); }; }
Da das noch nicht sehr komfortabel ist - schließlich muß ja vorher noch der Zugriff auf das Element erfolgen - erweitern wir unser JS um eine kleine Funktion, die diese Zuordnung einmalig für alle gewünschten Elemente übernimmt. Und hierbei kommt endlich unsere getElementsByClass Methode zum Einsatz. Wir werden unser Schalter-Element wahrscheinlich ohnehin später mit CSS formatieren, mit geeignetem Mauszeiger und einer Formatierung, die auf seine Funktion hinweist. Nichts liegt näher, als die dafür verwendete Klasse auch als Markierung für unsere JS Funktionalität einzusetzen. Es gilt also, alle Elemente mit der Klasseneigenschaft 'switcher' (siehe Ausgangsbeispiel) an die addToggleEvent Funktion zu übergeben. Und das geht so:
Code:
addToggleEvents = function () { var sMarkUp = 'switcher'; var oElements = getElementsByClass (sMarkUp); var iLength = oElements.length; for(var iCnt = 0 ; iCnt < iLength ; iCnt++) { addToggleEvent (oElements[iCnt]); } }
Code:
<html><head> <link rel="stylesheet" type="text/css" href="toggleDiv.css" /> <script type="text/javascript" src="toggleDiv.js"></script> </head><body> <p class="switcher" tgg_target="Info">+ Schalter</p> <div id="Info">Unser Infofeld</div> <script type="text/javascript"> addToggleEvents (); </script> </body></html>
Anregungen zur Erweiterung:
- - die fest zugewiesenen Attributbezeichner wie sTargetIdentifier können an einer zentralen Stelle des Scripts als globale Variablen außerhalb der Funktion gesetzt werden.
- der Fall, dass ein tgg_target Attribut nicht gesetzt ist oder auf eine nicht existierende ID verweist, sollte mit einem return false abgefangen werden. Danach könnte das class Attribut um einen Bezeichner erweitert werden, der erst die CSS Formatierungen bereitstellt, die die Toggle Funktion signalisieren.
- schließlich kann auch das auslösende Event selbst variabel gestaltet werden, z.B. auch über eine globale Variable oder sogar über einen Attribut-Wert im p Element. Hier verweise ich auf das zusammenfassende Scripting unten, wo ich das umgesetzt habe.
Schritt 4: Viva Autonomia - Modularität
Noch stört der Scriptaufruf am Ende. Schöner wäre doch, toggleDiv.js oben einzubinden und dabei schon automatisch die Toggle Funktion einzurichten. Das Problem ist dabei, dass alle Elemente bereits vorhanden sein müssen, die die onClick Event-Eigenschaft zugeordnet bekommen. Wir benötigen also einen Anstoß, wenn alle Elemente geladen sind. Genau das ermöglicht das window.onLoad Event. Eine Zeile in unserem toggleDiv.js genügt dafür:
Code:
window.onload = addToggleEvents;
Schritt 5: Keine Streitigkeiten am Tisch!
Die nächste und für dieses Tutorial letzte Frage wirft Drew McLellan in seinem Artikel Writing Responsible JavaScript[2] auf: JS als Layer - wunderbar, aber was passiert, wenn unser Konzept window.onload = addToggleEvents; auch in anderen JS 'Modulen' verfolgt wird? Jede Zuweisung a´la window.onload = ... wird die alte überschreiben und schließlich wird nur die Funktionalität des zuletzt eingebundenen JS Layers übrig bleiben.
Gottlob gibts auch dafür eine Lösung. Als - man könnte sagen - Quasistandard hat sich folgendes Fragment von Simon Willison[3] herausgebildet, das ich lediglich in meine Variablenschreibweise umgemünzt habe:
Code:
addOnLoadEvent = function (mFunction) { var mOldOnLoad = window.onload; if (typeof window.onload != 'function' ) { window.onload = mFunction; } else { window.onload = function () { if (mOldOnLoad) mOldOnLoad (); mFunction (); } } }
Das macht unser Modul komplett. Statt egoistisch window.onload = addToggleEvents; zu benutzen, geben wir auch anderen Modulen die Freiheit window.onload zu nutzen und ändern den Aufruf in addOnLoadEvent (addToggleEvents); ab.
Resultat (erweitert um einige Spirenzien)
Code:
<html><head> <link rel="stylesheet" type="text/css" href="toggleDiv.css" /> <script type="text/javascript" src="toggleDiv.js"></script> </head><body> <p class="switcher" tgg_target="Info">+ Schalter</p> <div id="Info">Unser Infofeld</div> </body></html>
Code:
.switcher {background-color:#bbb; margin:0; } #Info {background-color:#ff0; height:3em;width:20em; } .hidden {display:none; }
Code:
// onLoad Event hinzufügen - Funktion sollte als Core Methode in ein // separates externes JS addOnLoadEvent = function (mFunction) { var mOldOnLoad = window.onload; if (typeof window.onload != 'function' ) { window.onload = mFunction; } else { window.onload = function () { if (mOldOnLoad) mOldOnLoad (); mFunction (); } } } // Zugriff auf DOM Element über Klasseneigenschft - Funktion sollte als Core // Methode in ein separates externes JS getElementsByClass = function (sClassName) { var aReturnElements = []; var oRegExpClass = new RegExp ('\\s*\\b' + sClassName + '\\b'); var oElements = (document.all) ? document.all : document.getElementsByTagName ('*'); for (var i = 0 ; i < oElements.length ; i++) { if (oRegExpClass.test (oElements[i].className)) aReturnElements.push (oElements[i]); } return (aReturnElements); } // Settings für die Umschaltfunktion sTgg_DefaultMarkUpAttribute = 'switcher'; sTgg_DefaultTargetIdentifier = 'tgg_target'; sTgg_DefaultEvent = 'click'; toggleDivArea = function (sID , iStatus) { oElement = document.getElementById (sID); modifyClass (oElement , 'hidden'); } modifyClass = function (oElement , sValue , iStatus) { var iStatus = iStatus || -1; var oRegExpClass = new RegExp ('\\s*\\b' + sValue + '\\b'); if (-1 == iStatus) iStatus = ! oRegExpClass.test (oElement.className); switch (iStatus) { case true : case 1 : case '1' : oElement.className += ' ' + sValue; break; default: oElement.className = oElement.className.replace (oRegExpClass , ''); break; } } addToggleEvent = function (oElement , sTargetIdentifier) { var sTargetIdentifier = sTargetIdentifier || sTgg_DefaultTargetIdentifier; var sEvent = sTgg_DefaultEvent; var sTarget = oElement.getAttribute (sTargetIdentifier , false); oElement.removeAttribute (sTargetIdentifier , false); if (oElement.addEventListener) { oElement.addEventListener (sEvent.toLowerCase() , function () { toggleDivArea (sTarget); } , false); return (true); } else if (oElement.attachEvent) { var mReturn = oElement.attachEvent('on' + sEvent.toLowerCase() , function () { toggleDivArea (sTarget); }); return (mReturn); } else { return (false); } } addToggleEvents = function () { var sMarkUp = sMarkUp || sTgg_DefaultMarkUpAttribute; var oElements = getElementsByClass (sMarkUp); var iLength = oElements.length; for(var iCnt = 0 ; iCnt < iLength ; iCnt++) { addToggleEvent (oElements[iCnt]); } } // Funktionalität des Moduls einbinden addOnLoadEvent (addToggleEvents);
Quellen
[1] http://www.robertnyman.com/2005/11/0...tsbyclassname/
[2] http://24ways.org/2006/writing-responsible-javascript
[3] http://simonwillison.net/2004/May/26/addLoadEvent/
Und jetzt seid ihr dran...
Kommentar