Too Much Cookies Network

Dynamic Sorting of HTML-tables via XSLT

Sonntag, 27. August 2006, 22:51

[lplang lang="english"]

Deutsche Version

More than eight months ago - actually on newyear’s eve - i was sitting at my PC trying to find a solution to a question Ursula asked in wer-weiss-was1 about sorting data in HTML-tables without having to reconnect to the server. At the end of the night, i was able to make a suggestion in form of a XSL Transformation that was changed dynamically with a javascript function whenever the user selected a table header.

Looking back..

The suggested solution does work fine, but it has some flaws that make it rather hard to implement in a normal webdesign-case.

The most fatal problem about the solution was, that the data that had to be sorted would be loaded after the document was loaded. That way, someone running without javascript enabled, wouldn’t be able to see the document correctly. Also, search engines such as googlebot won’t be able to see any of the data presented.

Of course one could attach the table and delete it via javascript. This would mean that javascript-disabled clients would still be able to see the data and javascript-enabled clients would be able to dynamically sort the table. But this way one would load the data in the table twice. This could be a problem when one has to display a big table or if creating the table costs too much CPU-time on the server.

Another problem of the implementation is the inflexibility of the script. It is made exactly for the specific data and has to be changed for any other data resulting in a huge amount of coding required.

Alternative suggestion

In the latest iX-magazine there is an article about sorting HTML-tables via Javascript (Sept. 2006 | “Auf der Stelle: HTML-Tabellen mit Javascript sortieren”, p. 142). The author Christian Kirsch suggests saving all the data of a given table into an array and sorting it via array.sort(col) using the standard sorting algorithms of javascript.

In fact, he solves every problem i had with my implementation, but i still think, that it is more elegant to use XSL-Transformations, since they can be expanded to include filtering-capabilities which still can be implemented using normal javascript, but at the cost of huge code. Plus if one wants to sort by more than one column, the pure javascript-solution would be even bigger whereas the XSL-solution will only expand by one statement per sortcolumn or filter-criteria!

New Suggestion

Here’s my new suggestion: XSL-Sort for HTML-Tables

The basis of the implementation was a normal HTML-table. It has to be XHTML-compatible, as shown in the example below:

<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>4</td>
<td>Hans Jürgen</td>
</tr>
<tr>
<td>1</td>
<td>Otto Quatsch</td>
</tr>
</tbody>
</table>

Note that i’m using thead and tbody - elements! They are important or else i’d have to rewrite the table header every time i resort the table. Now to sort this markup-fragment i will use this xslt-file:

<xsl:stylesheet
xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” version=”1.0″>
<xsl:template match=”tbody”>
<html>
<body>
<table>
<tbody>
<xsl:for-each select=”tr”>
<xsl:sort select=”td[1]“ />
<tr>
<xsl:for-each select=”td”>
<td class=”{@class}”><xsl:value-of select=”current()” /></td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

The select-statement in the sort-element is the one that has to be changed programatically depending on which column-header the user selects. For more explanation on the select-statement refer to this wonderful reference.

Coding

The code is pretty much straight forward, though it could be irritating because it’s not very well structured. To begin with, the engine searches for any table inside the document and then installs eventlisteners in every column head and marks the column heads with a non-standard attribute colnumber. This is the easiest way i found to know which column to sort on. This is done in install_sorters() and install_sorter(mytable) respectively.

The onclick-Event of every column header calls the function sortfor(). Here, the colnumber-attribute of the event target (some column header) is read and used to change the xsl-DOM. It needs to only change the element xsl:sort:

// sort for column #
var mysort = xslt.getElementsByTagNameNS(XSLNS, “sort”).item(0);
mysort.setAttributeNS(null,”select”,”td["+colnumber+"]“);
// Perhaps sort as number??
colclass=col.getAttributeNS(null,”class”);
if (colclass.lastIndexOf(’sorttypenumber’)>-1)
mysort.setAttributeNS(null,”data-type”,”number”);
else mysort.removeAttributeNS(null,”data-type”);
// Alternating Sort order
sortorder=”ascending”;
if (table.getAttributeNS(null,”sortedfor”)==colnumber) {
if (table.getAttributeNS(null,”sortorder”)==”ascending”)
sortorder=”descending”;
}
mysort.setAttributeNS(null,”order”,sortorder);
// Now save the sort-parameters as attributes of table
table.setAttributeNS(null,”sortedfor”,colnumber);
table.setAttributeNS(null,”sortorder”,sortorder);

As can be seen in the last two lines, the script saves the sort-information as attributes in the table-element. This information is then used to determine which way to sort the next time the user selects a column header. Furthermore, it has to be able to differentiate between numeric and string columns, since sorting numbers as though they were strings would result in a weird row order. To determine the data-type of a specific column, the script reads the class-attribute of the column header. It expects that numeric columns are marked with the class “sorttypenumber”.

After that, there isn’t much to do: running the xsl-processor and replacing the old tbody-element with the sorted data.

// run the xslt-processor
processor.importStylesheet(xslt);
result = processor.transformToDocument(data.getElementsByTagName(”tbody”).item(0));
result=result.getElementsByTagName(”tbody”).item(0);
// and replace old data with sorted data.
var temp=table.getElementsByTagName(”tbody”).item(0);
table.replaceChild(result,temp);

Problems with Internet Explorer

As usual Microsoft has it’s own implementing theory that says:

Thou shall not be compatible!

I don’t want to get into details concerning the changes i had to make for the sake of Internet Explorer, but two points i’d like to mention:

  1. The eventlistener of the object window wasn’t willing to work. I tried the method attachEvent as well as simply assigning the event-function to the attribute onload. Both didn’t get me, where i wanted to be. But the attachEvent method can be set at the end of the main HTML-document. Strangely, not at the end of the javascript-file, which - logic would suggest - would be parsed after the main file..
  2. Replacing existing nodes with a string of xml-elements did seem to be rather easy, but it wasn’t, especially since i only need a fragment of the markup the xsl-processor outputs! I ended up running a regular expression search for the tbody-fragment inside the outerHTML-attribute of the online-table and replacing it with the xml-fragment from the processor. Just have a look at the code (around line 80) and you’ll see how much trouble i had inserting that small portion of markup.. If anyone has a better solution, please don’t hesitate to post a comment!

Conclusion

It is possible to have data, that is already loaded and parsed into the HTML-document resorted and - hopefully in the near future - filtered or otherwise manipulated without having to put too much into coding special-case functions. The script can easily be implemented into any existing page and make every table on that page sortable. At this time it requires the table to be xhtml-compatible and to have column headers. If some columns have to be sorted numerically, the script requires a special class to be given to the column header.

Of course, this script isn’t meant for badly designed sites where the element-placement is done via fake tables. It’s also not meant for tables with spanning cells (colspan or rowspan), since they will throw the xsl-transformation off-balance. Either way, it shouldn’t destroy any of the two cases, as long as they don’t employ table headers (and most don’t)..

Have fun and please do comment!

[/lplang]
[lplang lang="deutsch" default="default"]

English version

Schon vor mehr als 8 Monaten habe ich mich mit der dynamischen Sortierung von HTML-Tabellen beschäftigt und einen Lösungsvorschlag gemacht, mit dem sich erreichen lässt, dass in einer Tabelle nach einem beliebigen Feld sortiert werden kann.

Die Grundidee besteht darin, die Tabellendaten als XML-Daten nachzuladen und durch eine XSL-Transformation laufen zu lassen, die letztere sortiert. Bei Anwahl einer Spaltenüberschrift wird ein Script ausgeführt, das die XSL-Anweisungen verändert und die Transformation erneut laufen lässt und die “alten” Daten mit den sortierten ersetzt.

Nachteile

Der grösste Nachteil der bisherigen Lösung ist die Abhängigkeit des Nutzers von Javascript. Bei abgeschaltetem Javascript ist die Tabelle - und damit die Daten - nicht zu sehen. Während das für den normalen Benutzer relativ erträglich ist, da die meisten Nutzer Javascript eingeschaltet haben, ist das für eine Suchmaschinenoptimierung kontraproduktiv, da Such-Bots wie googlebot keine Scripte ausführen und somit nicht die Daten in der Tabelle indexieren können.

Natürlich kann man die Daten als statische Tabelle im HTML einfügen und bei eingeschaltetem Javascript die xml-Daten nachladen, allerdings bedeutet das die doppelte Übertragungsmenge und kann bei grösseren Tabellen eine starke Verlangsamung der Seite zur Folge haben.

Weiterhin ist das Script nicht sehr flexibel, da es für eine spezielle Tabelle geschrieben ist und für andere neu geschrieben werden müsste. Das bedeutet widerum viel mehr code als vertretbar wäre..

Array-Alternative

In der neusten iX findet sich ein Artikel mit dem Titel “Auf der Stelle - HTML-Tabellen mit Javascript sortieren”, der dasselbe Problem bespricht und einen Vorschlag zur Lösung macht (Sept. 2006, S. 142). Der Author Christian Kirsch schlägt vor, die Daten einer beliebigen Tabelle komplett in ein Array einzulesen, darin mittels der Methode .sort(col) zu sortieren und danach wieder in die Tabelle zu schreiben.

Während dieser Vorschlag alle von mir angesprochenen Nachteile meines scripts löst - es ist nicht auf eine Tabelle bezogen und die Daten sind bei abgeschaltetem Javascript sichtbar - glaube ich, dass die Lösung im Vergleich nicht sehr elegant ist und möglicherweise langsamer als die xsl-gestützte Sortierung. Weiterhin ist es mittels xsl möglich das Script weiter aufzuwerten, um etwa nach bestimmten Suchkriterien zu filtern oder nach mehr als einer Spalte zu sortieren, während das mittels javascript zwar auch möglich, aber mit sehr komplexem code verbunden ist.

Neuer Ansatz

Hier ist mein neuer Vorschlag: XSL-Sort for HTML-Tables

Die Grundlage für die Umsetzung bildet eine standardmässige HTML-Tabelle, die über Spaltenüberschriften und xhtml-kompatibles tbody-Element verfügt.

<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>4</td>
<td>Hans Jürgen</td>
</tr>
<tr>
<td>1</td>
<td>Otto Quatsch</td>
</tr>
</tbody>
</table>

Es bietet sich an, auch das thead-Element zu benutzen, um den Überschriften-Teil der Tabelle vom Datenteil syntaktisch zu trennen.

Um diesen Teil der Seite zu sortieren, kann man sich eines xsl-Fragments bedienen, wie er weiter unten aufgelistet ist:

<xsl:stylesheet
xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” version=”1.0″>
<xsl:template match=”tbody”>
<html>
<body>
<table>
<tbody>
<xsl:for-each select=”tr”>
<xsl:sort select=”td[1]“ />
<tr>
<xsl:for-each select=”td”>
<td class=”{@class}”><xsl:value-of select=”current()” /></td>
</xsl:for-each>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

Interessant ist hier die Benutzung der select-Anweisung im sort-Element. Das sort-Element bezweckt, dass die Daten nach dem Inhalt der ersten Spalte sortiert werden. Die Zahl in den eckigen Klammern gibt somit direkt die zu sortierende Spalte an. Für mehr Informationen zur sort-Anweisung, empfehle ich die folgende Seite: XPath Expression Syntax.

Javascript

Zu allererst muss dafür gesorgt werden, dass alle Spaltenüberschriften einen Eventlistener verpasst bekommt, der erst bei Selektierung dieser Überschrift die Sortierung einleitet. In den Funktionen install_sorters() und install_sorter() werden in allen Spaltenüberschriften in allen Tabellen diese Eventlisteners installiert und weiterhin über ein nicht-standard-Attribut colnumber die Spaltennummer übergeben, die für die Sortierung gebraucht wird.

Die onclick-Events der Spaltenüberschriften rufen die Funktion sortfor() auf. Darin wird das Attribut colnumber ermittelt und für die Manipulation der xsl-Transformation xsl:sort verwendet.

// Nach Spalte # sortieren
var mysort = xslt.getElementsByTagNameNS(XSLNS, “sort”).item(0);
mysort.setAttributeNS(null,”select”,”td["+colnumber+"]“);
// Als Zeichenfolge oder als Zahl sortieren?
colclass=col.getAttributeNS(null,”class”);
if (colclass.lastIndexOf(’sorttypenumber’)>-1)
mysort.setAttributeNS(null,”data-type”,”number”);
else mysort.removeAttributeNS(null,”data-type”);
// Aufsteigend oder absteigend sortieren?
sortorder=”ascending”;
if (table.getAttributeNS(null,”sortedfor”)==colnumber) {
if (table.getAttributeNS(null,”sortorder”)==”ascending”)
sortorder=”descending”;
}
mysort.setAttributeNS(null,”order”,sortorder);
// Nun noch einige Sortier-Parameter in die zugehörige Tabelle speichern..
table.setAttributeNS(null,”sortedfor”,colnumber);
table.setAttributeNS(null,”sortorder”,sortorder);

Wie man den letzten zwei Zeilen ansehen kann, werden weitere zwei weitere Parameter in die DOM gespeichert. Beide dienen der Ermittlung bei der nächsten Selektion einer Spaltenüberschrift, in welche Richtung sortiert werden soll (absteigend oder ansteigend). Die Speicherung in Element-Attribut sichert eine gänzliche Abkoppelung des Scripts vom tatsächlichen HTML.

Weiterhin verwendet das Script das Klasse-Attribut der Spaltenüberschriften für die Ermittlung des Datentyps der Spalte. Wenn sich ein Eintrag “sorttypenumber” in der Klassenangabe wiederfindet, dann ist die Spalte numerisch zu sortieren!

Der Schluss bildet die Verarbeitung der xsl-Anweisungen und das Austauschen der “alten” Daten gegen die neu sortierten Daten. Beides findet sich ähnlich schon im letzten Vorschlag wieder.

// run the xslt-processor
processor.importStylesheet(xslt);
result = processor.transformToDocument(data.getElementsByTagName(”tbody”).item(0));
result=result.getElementsByTagName(”tbody”).item(0);
// and replace old data with sorted data.
var temp=table.getElementsByTagName(”tbody”).item(0);
table.replaceChild(result,temp);

Probleme mit dem Internet Explorer

Wie sooft braut sich Microsoft mit ihrer Nicht-Umsetzung von w3c-Vorgaben ihre eigene Suppe. Ich möchte nicht in die Details einsteigen, sondern nur zwei Punkte ansprechen:

  1. Der Eventlistener des Objekts window wollte partout sich nicht ordentlich setzen lassen. Ich habe es über attachEvent, sowie über die statische Vergabe des Attributs onload versucht - ohne Erfolg. Die attachEvent-Methode hat aber funktioniert, als ich sie ans Ende der HTML-Datei brachte. Der gesunde Menschenverstand sagt doch aber, dass die Javascript-Datei (die ja erst noch eingebunden werden müsste) später ausgeführt wird als Skripte in der HTML-Datei..
  2. Die Ersetzung der alten Nodes mit den sortierten Daten war im IE nicht so einfach wie ich annahm, besonders da ich nur einen Teil der von der xsl-Engine zurückgegebenen Zeichenfolge einbinden möchte. Für Lacher wird also meine eher eigenartige Lösung sorgen. Einfach mal um Zeile 80 herum schauen..

Schlussfolgerung

Es lassen sich bereits geladene und eingebundene Tabellen im nachinein ohne weitere Verbindung zum Server und ohne besonders angefertigte Funktionen neu sortieren und - hoffentlich in naher Zukunft- filtern oder anderweitig manipulieren. Das Skript lässt sich relativ leicht für andere Seiten verwenden und kann idealerweise durch einfache Einbindung und Beachten der Grundstrukturen dafür sorgen, dass alle Tabellen auf einer beliebigen Seite sortierbar werden.

Natürlich wird das Skript versagen, wenn es auf Seiten angewandt wird, die aus weit verschachtelten Tabellen als Designersatz bestehen. Auch wird die Sortierung von Tabellen mit zusammengefassten Zellen (über die Attribute colspan oder rowspan) nicht ohne weiteres funktionieren. Auf der anderen Seite sollten beide Sorten von Tabellen nicht weiter vom Skript zerstört werden, solange sie nicht über Spaltenüberschriften verfügen.

Viel Spass und bitte kommentieren..

[/lplang]


  1. roughly who-knows-what, a very nicely structured german internet forum []
5 Kommentare

Kommentar von dfcnt

Made Donnerstag, 2 of November , 2006 at 16:53

hmm sehr interessant, großartig! hab mal ein wenig mit herumgebastelt, funktioniert soweit sehr gut mit dynamischen daten aber z.b. links werden nach dem sortieren nicht mehr richtig erkannt.. bzw sind nicht mehr klickbar… hast du da vielleicht einen tip für mich? danke :-)

Kommentar von Omar Abo-Namous

Made Donnerstag, 2 of November , 2006 at 17:41

Hallo dfcnt,

danke für die Frage. Folgende Änderung ist nötig: Der Prozessor muss den kompletten Zweig unterhalb von <td&gt stur kopieren. Das tut er, indem man allgemeine Templates darauf anwendet:

Schau dir jetzt das Beispiel noch einmal an und die Datei style.xml (leider gibt es ein Problem beim IE.. muss ich nachher mal sehen..)

Pingback von CSS-Based Tables: Modern Solutions | Smashing Magazine

Made Freitag, 29 of Dezember , 2006 at 14:15

[...] Dynamic Sorting of HTML-tables via XSLT - design50 [...]

Pingback von CSS-Based Tables: Modern Solutions

Made Sonntag, 3 of Juni , 2007 at 11:45

[...] Dynamic Sorting of HTML-tables via XSLT [...]

Pingback von CSS referinţe | Web Mastery

Made Mittwoch, 3 of Oktober , 2007 at 13:44

[...] Dynamic Sorting of HTML-tables via XSLT [...]

Kommentar hinterlassen

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

About

Seit November 2004 berichtet 'Too Much Cookies Network' live und radikal aus der Parallelgesellschaft. Die Themenwahl ist willkürlich, der Sprachstil filigran und der Gegner unklar. Zum Netzwerk gehören weiterhin folgende Seiten: