Business Server Pages und Prototype

In Web-Anwendungen kommt der clientseitigen Logik in Form von JavaScript eine tragende Rolle zu: Nicht alle Präsentierungsfeatures lassen sich statisch in Form von CSS- und HTML-Code abbilden. Wurde JavaScript ursprünglich als "Web Site Glue" eingesetzt, als Workaround für fehlende HTML-Features und für kleine clientseitige Zusätze, so gewann JavaScript spätestens seit dem Aufstieg der "Ajax"-Techniken enorm an Bedeutung. Nun werden ganze Seiteninhalte dynamisch erzeugt oder Teile davon ausgewechselt, ganze Applikationen in JavaScript programmiert.

JavaScript nimmt heute häufig den Platz ein, der einmal den Java-Applets zugedacht war, auf dem diese aber gescheitert sind: Das von HTML abgedeckte UI um andere, auf einen konkreten Anwendungskontext zugeschnittene Elemente zu erweitern. JavaScript ist im Gegensatz zu Java auf jedem gängigen Browser ohne spezielle Einstellungen oder Installationen verfügbar, und es ist leichtgewichtig. Man muss keine tonnenschwere Laufzeitumgebung laden, bevor eine Codezeile auf dem Client ausgeführt werden kann (siehe zu diesem Thema auch meinen Blog Java und JavaScript vom 29.3.07). JavaScript wird nicht nur als Workaround für browserübergreifende Präsentierungsfunktionalität eingesetzt (siehe unten das Beispiel der Unterdrückung von Leerzeilen), sondern auch zur Validierung von Eingabedaten: Um etwa zu prüfen, dass in einem Feld etwas eingegeben wurde oder ob die Eingabe in ein Feld einen Währungsbetrag darstellt, ist es meist unnötig, einen Server-Roundtrip durchzuführen.

In Business Server Pages haben wir den gesamten an den Client gesendeten HTML-Tree unter Kontrolle. Das bringt den grossen Vorteil, dass wir nützliche Technologien und Erweiterungen für Web-Anwendungen sofort einsetzen können.

Das JavaScript-Framework Prototype braucht in der aktuellen Version nur rund 120 KB - und hat dafür einen erstaunlichen Leistungsumfang. Nicht nur die syntaktischen Erweiterungen der Sprache JavaScript selbst, sondern auch die DOM-Erweiterungen von Prototype haben es in sich. In diesem Artikel beschreibe ich eine Auswahl der vielen Features des Prototype-Frameworks.

each(), map(), any() und all() statt "Vanilla Loops"

Sicher haben auch Sie schon eine "Vanilla Loop" programmiert. Obwohl Vanille ja eigentlich etwas Leckeres ist, ist es als Beiwort im Englischen oft pejorativ gemeint: Vanille trifft den Durchschnittsgeschmack, ist etwas Hundsgewöhnliches, alle mögen es, alle machen es so, ohne viel nachzudenken. Für Sam Stephenson, den Autor des Prototype-Frameworks, sind Vanilla Loops indexbasierte Schleifen wie die folgende:
var i,a = [10,20,30], total = 0;
for (i=0;i<maxLength;i++) {
// Addiere das i-te Element des Arrays a zu total
  total += a[i];
  }
So einfach und unverdächtig eine solche Schleife aussieht, ist das Arbeiten mit Indices doch eine potentielle Fehlerquelle. Häufig vergisst man die Deklaration der Hilfsvariablen i, wodurch ungewollt eine globale Variable eingeführt wird. Gelegentlich stolpert man auch über das "Zaunpfahlproblem" (indem man die 17 Zaunpfähle mit den 16 Zwischenräumen zwischen ihnen durcheinanderbringt) und über die Frage, ob die Schleife bei 0 oder 1 zu beginnen ist. Die Indices in ihren eckigen Klammern erschweren auch die Lesbarkeit und verschleiern die eigentliche Absicht. Was man eigentlich tun will, ist doch: Für jedes Element des Arrays eine Operation ausführen. Das drückt man in Prototype viel deutlicher mit der Funktion each() aus. Obiges Beispiel, die Elemente eines Array aufzuaddieren, kann man in Prototype wie folgt notieren:
var a = [1,2,3], total = 0;
a.each( function(item) {
    total += item;
  });
Die klassischen Arrays sind in allen gängigen Browsern erweiterbar. Daher kennt das Objekt [1,2,3] in diesem Beispiel die Funktion each(), sobald die Prototype-Bibliothek geladen und ausgeführt wurde.

Funktionen, die etwas komplexer sind, wird man nicht wie in obigem Beispiel anonym deklarieren, sondern mit einem eigenen Namen versehen. Dann genügt der each()-Funktion einfach dieser Bezeichner. Wenn die Freiheit in der Benennung geeignet gewählt wird, steigert dies darüberhinaus weiter die Lesbarkeit. Die Funktion aus obigem Beispiel ist natürlich zu kurz - es wäre i.a. Verschwendung, einen Einzeiler in eine separate Funktion auszulagern. Aber selbst in diesem Beispiel erhöht sich noch die Lesbarkeit:

var a = [1,2,3], total = 0;
function addToTotal( x ) { 
  total += x; 
  }
a.each( addToTotal );  
Manchmal gibt es den Fall, dass aus einem Array ein neuer gebildet werden muss. Dann bietet sich statt each() die Funktion map() an. Diese bildet einen Array aus den Ergebnissen, die sich durch Anwendung der Funktion auf die einzelnen Array-Elemente ergeben. Nehmen wir an, wir wollen einen neuen Array bilden, indem wir aus jedem Element eines gegebenen Arrays die Quadratwurzel bilden. Mit map() geht das kurz und bündig - wir müssen der map() nur die auszuführende Funktion mitteilen, in diesem Fall die eingebauet Funktion Math.sqrt:
var a = [1,2,3], b;
b = a.map( Math.sqrt );  // ergibt b = [1, 1.414..., 1.732... ]
Wem die Funktion map() aus der Programmiersprache Perl bekannt vorkommt, der wird sich nicht wundern, auf weitere alte Bekannte zu treffen. Auch die Perl-Funktion qw zum Aufbau eines Arrays aus durch Leerraum getrennten "Wörtern" gibt es in Prototype - sie heisst hier $w(). Der Ausdruck
$w('html css javascript')
ist daher äquivalent zu folgendem, aber etwas besser lesbar:
['html','css','javascript']
Manchmal muss eine Bedingung für alle Elemente eines Arrays überprüft werden. Hierfür gibt es die Funktionen any() und all():
var allPositive = [1,2,3].all( function(x){ return x > 0; }) // -> true 
var someNegative = [1,-2,3].any( function(x){ return x < 0; }) // -> true
Sehr nützlich ist auch die Funktion zip(), die zwei Arrays nach dem Reissverschlussverfahren zu einem neuen Array von Paaren zusammenfügt:
var a=$w('a b c'), b=[1,2,3], c={};
a.zip(b).each( function( pair ) { c[pair[0]] = pair[1]; } )
Wenn wir auch hier den Zugriff auf Indices vermeiden wollten - wie könnten wir das erreichen? Das erweiterte Objekt Hash von Prototype bietet zwar eine Methode set(name,value) an, um dem Hash ein Name/Wert-Paar hinzuzufügen. Aber es wäre schön, eine überladene Version zur Verfügung zu haben, die bei
c.set( ['a',0] );
dem Hash c das Name/Wert-Paar 'a':0 hinzufügt. Für Aufgaben wie Überladen gibt es in Prototype die schöne Funktion wrap. Wir können die Methode set mit wrap leicht um das gewünschte Verhalten erweitern:
Hash.prototype.set = Hash.prototype.set.wrap(
  function( proceed, name, value ) { 
    if ( Object.isArray( name )  && (name.length == 2) ) 
      return proceed(name[0],name[1]);
    else return proceed(name, value);  
    }
  );  
Wir sind die Indices zwar nun nicht los, aber wir haben sie an einer einzigen Stelle, üblicherweise einer sehr zentralen, hinterlegt und benötigen danach im Anwendungscoding keine Indexzugriffe mehr. Mit der solchermassen erweiterten Funktion Hash.set() schreibt sich das obige Beispiel nun wie folgt:
var a=$w('a b c'), b=[1,2,3], c = new Hash();
var addPairToHashC = Hash.prototype.set.bind(c);
a.zip(b).each( addPairToHashC );
Die Funktion addPairToHashC bindet das implizite Objekt this der Implementierung von Hash.set() an den konkreten Hash c dieses Beispiels. Da diese Funktion für alle Schleifendurchläufe gleich bleibt, kann man sie vorab definieren (wie immer mit einem sprechenden Namen) und dann als Argument der each()-Funktion angeben.

Navigation im DOM

Zwei Dinge machen das Prototype-Framework für das Arbeiten mit dem Document Object Model (DOM) der HTML-Seite besonders geeignet: Zum einen die Adaption der CSS-Selektorlogik in die Utility-Funktion $$(), die erweiterte Elementfunktion select() und in die DOM-Navigationsfunktionen. Zum anderen die browserübergreifende Realisierung der DOM-Navigationsfunktionen up(), down(), next(), previous(), ancestors(), childElements(), firstDescendant() als Navigation im HTML-Elementbaum, das heisst bei konsequenter Nichtberücksichtigung der Text- und Kommentarknoten im Dokument. Wo immer es möglich ist, akzeptieren diese Funktionen auch einen Index und/oder einen CSS-Selektor für die Tiefensuche.

Enthält das Dokument beispielsweise eine Tabelle mit der ID orderItems, so liefert die Funktion

$('orderItems').down('td.positive')
die erste Zelle mit der CSS-Klasse positive. Dagegen würde
$('orderItems').select('tr.zebra1 td.positive')
eine Collection aller Zellen der CSS-Klasse positive liefern, die in Tabellenzeilen der CSS-Klasse zebra1 enthalten sind. Eine solche Collection würde man in Prototype dann idealerweise mit einer der Funktionen each(), map(), all() oder any() durchlaufen.

Die browserübergreifende Implementierung und die gute Wahl der Namen und Schnittstellen erleichtern es, auf Basis von Prototype gut lesbaren JavaScript-Code zu verfassen.

Einsatz von Ajax - eingebunden in die BSP-Anwendungen

Ziel: Man befindet sich mitten in einer Applikation und möchte, ohne einen Seitenwechsel auszulösen, eine Aktion im SAP-System ausführen und nach Ausführung bestimmte Teile des HTML-Dokuments austauschen.

In diesem Fall empfiehlt sich, im Ajax-Request einen Fcode der aktuellen BSP-Applikation auszulösen, der in der Antwort nur ein HTML-Fragment zurücksendet. Dieses wird nach Abarbeitung des Requests dynamisch in die Seite eingebaut. Normalerweise macht man dies, indem man den vom Server empfangenen HTML-Code in ein anfänglich leeres <div>-Element stellt. Da dies ein so häufiges Vorgehen ist, gibt es hierfür die Prototype-Methode Ajax.Updater, die Request und HTML-Seitenupdate in einer einzigen Anweisung zusammenfasst. Im einzelnen geht man folgendermassen vor:

Einsatz von Ajax - unabhängig von der aktuellen Anwendung

Dieser Fall ist gar nicht so selten wie es scheint: Alle Wertehilfen gehören in diesen Bereich, auch das Einspielen von Zusatzinfos aus abhängigen Applikationen, zum Beispiel die Anzeige eines Langtextes zur Artikelnummer. In diesem Fall benötigt man serverseitig gar nicht das BSP-Framework, sondern kann den HTML-Antwortcode in einem eigenständigen Requestbehandler erzeugen.

Auf dem Weg über einen eigenen Requestbehandler mit einer kleinen, passgenauen Behandlerklasse lassen sich extrem kurze Antwortzeiten erzielen, vor allem wenn der HTML-Code mittels einer XSLT-Transformation oder - in ganz einfachen Fällen - durch Stringberabeitung (replace oder Templates) erzeugt wird.

Es ist allerdings zu beachten, dass bei Angabe einer vollen URL die "URL-Mangling" Information verlorengeht, d.h. die über Benutzer und Passwort hinausgehenden SAP-Anmeldeattribute, die vom SAP-Server in Klammern in das erste Element des URL-Pfads eingebaut werden. Man kann dieses bei Bedarf auch mit Hilfe eines regulären Ausdrucks aus der aktuellen URL document.location.href extrahieren und in die Ajax-Ziel-URL einbauen. Es kann sonst vorkommen, dass der Ajax-Request im Defaultmandanten oder in einer anderen als der Anmeldesprache ausgeführt wird - mit unerwünschten Ergebnissen.

Validierungen

Es ist ein sinnvolles Vorgehen, alle submit()-Aufrufe von Formularen einer Webseite durch eine zentrale Funktion ausführen zu lassen. Dann können an einer Stelle gemeinsame Aufgaben definiert werden. In unserem MVC-Framework ist dies die Funktion genericSubmit(), eine der wenigen globalen JavaScript-Funktionen. Sie setzt beispielsweise das globale Flag isBusy, mit dem eine Mehrfachausführung des Requests verhindert werden kann.

Diese Funktion genericSubmit() ruft nun auch dynamisch eine Funktion clientCheck() auf, falls auf der aktuellen Webseite eine solche Funktion deklariert wurde. Gibt diese Funktion den Wert false zurück, so wird der submit nicht ausgeführt. Man kann also eine solche Funktion definieren, um einfache Eingabeprüfungen vorzunehmen. Hier ein konkretes Beispiel, das die wesentlichen Ideen zeigt:

function clientCheck() {
          
  try {
  
    if ( $("chk_kaufbeleg").checked ) {
      if ( validate_amount($("preis")) <= 0 ) {               
        throw [msgText.ZM063,"preis"];
        }
      }                      
            
    if ( validate_amount( $("rabatt")) <= 0 ) {
      throw [msgText.ZM055, "rabatt"];
      }                    
      
    if ( $F("aufnr").blank() ) {  
      throw [msgText.ZM064, "aufnr"];                        
       }
              
    return true;
    }
  
  catch (e) {
    show_error(e);
    return false;        
    }          
  
  }
Folgende Strukturelemente wurden in dieser Validierungsroutine verwendet, die ich in der JavaScript-Programmierung bei BSP's für nützlich oder sogar unverzichtbar halte: Auch bei dem "Type-Sniffing", das für die Realisierung dieser verschiedenen überladenen Definitionen nötig ist, steht das Prototype mit vielen nützlichen Funktionen zur Seite. Methoden wie Object.isString(), Object.isArray() u.a., helfen bei dynamischen Typerkennung. Die Hilfsfunktion $A() kann auch die gesamte, im Array arguments gehaltene Parameterliste in einen erweiterten Array wandeln.
function show_error( iMessage, iFields) {
          
// Überladene Definition: show_error( [iMessage, iFields] ) -> show_error( iMessage, iFields )          
  if ((arguments.length == 1) && Object.isArray(iMessage)) {
    return show_error( iMessage[0], iMessage[1] );
    }
    
// Überladene Definition: show_error( "m", "f1", "f2", ... ) -> show_error( "m", "f1 f2" )          
  if (arguments.length > 2) {
    return show_error( iMessage, 
      $A(arguments).slice(1).flatten()
        );      
    }
          
// Fehlermeldung anzeigen                       
  setMessage("msg", iMessage);        
  
// Fehlerfelder markieren
  if (iFields) { 
    set_error_fields( iFields );  
    }          
  
  }

Beispiel eines Workarounds: Leere Zeilen einer Tabelle unterdrücken

Nicht nur für Prüflogik oder dynamische Server-Requests wie AutoCompletion ist JavaScript ein grosser Helfer. Er kann auch bei Layoutfragen helfen und Lücken in den Browserimplementierungen füllen.

Hierzu ein Beispiel: Für automatisch generierte HTML-Tabellen ergibt sich oft das Problem, dass leere Zeilen nicht wegkomprimiert werden. Die nachfolgende Tabelle verdeutlicht das Problem: Nach einer Zelle mit dem Text "Anfang" folgen acht Zeilen ohne Inhalt und schliesslich die "Ende"-Zeile. Die inhaltlosen Zeilen erhalten aber trotzdem eine kleine vertikale Höhe. Das ist störend. Am schönsten wäre es, wenn der Generator, der die Tabelle erzeugte, diese Situation selbständig erkennt und die Zeilen ohne Inhalt gar nicht mehr produziert. Das lässt sich aber nicht immer erreichen.

Anfang
Ende
Die CSS-Zuweisung empty-cells:hide hilft ein bisschen: sie unterdrückt wenigstens die Anzeige des Randes von Tabellenzellen, die nur Leerraum (Leerzeichen oder Tabulatoren) enthalten. Aber erstens wird sie auch im Internet Explorer 7 noch nicht interpretiert, und zweitens löst diese Angabe das Problem nicht vollständig, da noch etwas vertikale Höhe erhalten bleibt. In Prototype schreibt man einfach hin, was man haben möchte - zum Beispiel, dass alle Zeilen, die nur Zellen ohne Inhalt enthalten, aus dem Dokument entfernt werden sollen:
$$('#tbl_with_empty_rows tr' ).each( function(row) {
      if (row.descendants().all( Element.empty )) { row.remove(); }
    });          
Wenn Sie diese Funktion in Ihr onload-Script aufnehmen, übernimmte JavaScript das Aufräumen der Tabelle.

"empty-cells:hide" einschalten (im IE keine Reaktion!)   |   Leere Zeilen mit JavaScript-Mitteln entfernen   |   Acht leere Zeilen einfügen

Eine Tabelle dynamisch aufbauen

Haben Sie sich schon einmal gefragt, was in JavaScript so als gleich (==) betrachtet wird?

Bei so vielen Vertössen gegen das Prinzip der kleinsten Überraschung sollte man sich gegen weitere potentielle Überraschungen absichern. Zum Beispiel durch Erstellung einer Matrix, die uns mit roten Kästchen anzeigt, welche Vergleiche nicht erfüllt und mit grünen, welche erfüllt sind.[1]

Mit der JavaScript-Funktion buildTable(), die ich gleich zeigen und kommentieren werde, erhalten wir folgendes Ergebnis:

Die Spalten mit den Argumenten selbst zeigen, wie eine Evaluierung des Elements in einer Booleschen Abfrage ausfällt, etwa in

if (-1) {  /* Wird durchlaufen */ }
var x = Infinity || 7 ;  // Ergibt x = Infinity
Die Ergebnistabelle ist nicht besonders intuitiv: Auch wenn alle Ergebnisse der ECMAScript-Spezifikation entsprechen, wirkt es eher wie ein Bug, wenn etwa der String "Infinity" sich wie der vordefinierte Wert Infinity verhält, während dies für alle anderen vordefinierten Werte wie null, undefined usw. nicht der Fall ist. Einige Regeln sind es immerhin wert, sie sich zu merken: Doch nun zur Funktion, die die Tabelle generiert. Ich habe versucht, die Variablen und Parameter so zu benennen, dass der Code sich selbst kommentiert:
function buildTable(tableBody) {

  var testValues = [  null,  undefined,  true,  false,  -1,  0,  1,  NaN,  Infinity, 
                      '', ' ',
                      'null','undefined','true','false','-1','0','1','NaN','Infinity' ];
   
  var titleCell = function( iValue ) {
    return new Element('td')
        .addClassName( iValue ? 'true' : 'false' )
        .update( Object.inspect( iValue ) );
    }; 
              
  
  var titleRow = new Element( 'tr' )
                    .insert( new Element( 'td' )
                                   .update( '==' ) );  // Ecke oben links: '=='
  
  testValues.each( function( xValue ) {
    titleRow.insert(
      titleCell( xValue )
        );
    } );
    
  tableBody.insert( titleRow );  
         
  testValues.each( function( yValue ) {

    var row = new Element( 'tr' );
    row.insert(
      titleCell( yValue ) 
      );

    testValues.each( function( xValue ) {
      row.insert(
        new Element( 'td' )
         .addClassName(  xValue == yValue ? 'true' : 'false' ) 
         );
      });
      
    tableBody.insert(row);  
    
    } );
  
  }
In meinen Augen gewinnt der Code im Vergleich zu klassischem JavaScript — Sam Stephenson würde es wahrscheinlich "Vanilla JavaScript" nennen — deutlich an Lesbarkeit, wenn die Syntaxkonstrukte von Prototype eingesetzt werden: [1] Die Idee zu einer solchen Tabelle stammt von Nick Berardi: Fun with wacky JavaScript type comparisons. Für mich ist die Tabelle vor allem eine gute Gelegenheit, an einem obendrein noch nützlichen Beispiel die Möglichkeiten von Prototype vorzuführen.

Zurück