Anwendungen mit Ajax aufpolieren

Wir zeigen Ihnen, wie mit Ajax auch eine in die Jahre gekommene PHP-Applikation ohne große Eingriffe aufgefrischt werden kann, um die Benutzerfreundlichkeit zu verbessern. Das Werkzeug: die PHP-Bibliothek Sajax.

 

Bei der Applikation handelt es sich um ein CMS, in dem Beiträge verwaltet werden. Auch die Zuordnung zu Schlagwörtern ist dort vorgesehen, um die Texte thematisch selektiert präsentieren zu können.

Sajax-Beispiel

Autosave sichert Texte

Der älteste Teil des CMS ist das Änderungsformular für Beiträge. Von dem Anwender wurde zum Beispiel oft eine Autosave-Funktion vermisst. Denn vielen ist es schon passiert, dass Sie einen Text so lange im Browser offen gelassen haben, dass der Session-Timeout eintrat und das Absenden des Eingabeformulars mit einer Fehlermeldung beantwortet wurde, weil für das System die Zugangsberechtigung nicht mehr erkennbar war.

Autosave soll nun per Ajax nachgerüstet werden. Das Speichern muss also in regelmäßigen Abständen ohne störende Nebeneffekte während der Eingabe erfolgen. Dazu wird per Javascript ein Timerintervall definiert, das alle zwei Minuten abläuft und die Speicher-Funktion save_text() aufruft.

Zu Beginn des Skripts wird dem Sajax-Framework der Funktionsname “save_text” bekannt gemacht. Das bewirkt, dass Sajax auf der PHP-Seite, die die Anfragen beantwortet, eine solche Funktion vorfinden muss und auf der Client-Seite eine fertige Funktion x_save_text() zur Verfügung stellt. Die erwartet genau die Parameter des PHP-Pendants sowie als zusätzlichen Parameter den Namen der Funktion, die in Javascript die von PHP kommende Antwort auf den Funktionsaufruf auswerten soll.

sajax2-142

Der Speicherfunktion werden die aktuellen Felder des Beitrags übergeben. Der besseren Übersicht wegen wurde die tatsächlich vorhandene Feldliste stark zusammengestrichen. Neben der eindeutigen Id des Textes werden nur die Headline (head) und der Textkörper (body) übertragen.

Bei einfachen Problemstellungen würden Sie direkt die von Sajax erzeugte Funktion x_save_text() aufrufen. Im Falle des Speicherns gibt es aber gute Gründe, noch die Javascript-Funktion save_text() dazwischen zu schalten. Denn das automatische Speichern und das vom Anwender bewusst ausgelöste “Speichern & Ende” sollen bis auf das Verlassen des Formulars dieselbe Aktion bewirken. Es wäre unschön, diese Funktion zweimal zu implementieren. Also führen wir einen weiteren Parameter bLeaveForm ein, der beim Setzen auf 1 das Beenden des Formulars bewirken soll.

Es gibt damit zwei verschiedene Aufrufer der Funktion: Das Click-Event des Speichern-Knopfes und die Intervall-Steuerung. Um nun nicht an beiden Stellen die relativ lange Liste mit den Parametern aufführen zu müssen, rufen beide save_text() mit dem einzigen Parameter bLeaveForm auf. Erst in save_text() werden die aktuellen Werte der Formularfelder ermittelt. In der PHP-Funktion wird dann die eigentliche Arbeit erledigt, also die Datenbank mit den aktuellen Feldwerten geupdatet. Um im Listing Platz zu sparen, ist das nur andeutungsweise ausgearbeitet.

Als Feedback wird aus PHP eine Erfolgsmeldung der Sicherung zusammen mit einem Zeitstempel sowie der Wert von bLeaveForm zurückgegeben. Die Auswertung dieser Daten in Javascript übernimmt save_text_cb(). Ihr Name wurde Sajax über den letzten Parameter beim Aufruf von x_save_text() mitgeteilt. Diese Callback-Funktion zeigt im Formular den ermittelten Statusstring an. Zusätzlich bewirkt sie, dass der Browser zur Startseite umschaltet, falls bLeaveForm einen Wert von 1 hat. Das Durchreichen dieses Parameters an die Callback-Funktion mag umständlich erscheinen, aber die Verwendung einer globalen Javascript-Variable zu diesem Zweck wäre auch nicht durchsichtiger.

Stichwörter sollen herausstechen

sajax3-143

Ein teilweise optisches Problem ergab sich bei der Stichwortvergabe des Systems. Die war als Liste von beschrifteten Checkboxen realisiert. Bei der inzwischen stattlichen Anzahl von Keywords war die Liste sehr unübersichtlich geworden. Das neue Konzept sieht nun vor, dass jedes Keyword im Formular über per CSS-Klasse zugeordnete Hintergrundfarbe den Zustand anzeigt, damit der Benutzer die aktivierten Begriffe viel einfacher erkennt. Beim Anklicken löst ein Stichwort per Ajax einen Zustandswechsel aus. Sobald die Erfolgsnachricht aus der PHP-Schicht eintrifft, wird in der jeweiligen Callback-Funktion die CSS-Klasse gewechselt, um dem Benutzer den neuen Zustand anzuzeigen.

Der Ajax-Teil der Lösung ist ganz einfach: Es gibt für die Aktivierung und Deaktivierung eines Stichworts je eine Funktion, die als Parameter lediglich die Kennungen von Beitrag und Keyword übergibt. Wieder haben wir die Ausarbeitung der notwendigen Aktionen auf Datenbank-Ebene im Listing nur durch Kommentarzeilen angedeutet, um das Ganze nicht zu sehr aufzublähen.

Etwas trickreicher ist die Realisierung der Anzeige eines Statuswechsels. Die Stichwörter werden jeweils in <li>-Tags gepackt. Je nach Zustand bekommen sie eine der CSS-Klassen set_off oder set_on zugewiesen. Ihre HTML-ID besteht aus dem Präfix “kw_” und der Datenbank-Id des Stichworts. So kann eine einzige OnClick-Funktion alle Stichwörter behandeln. Über den Class-Namen eines Elements erkennt die Funktion den aktuellen Status und kann durch Auswertung der HTML-ID die Datenbank-Identifizierung des Keywords durchführen.

Verwandte Beiträge verknüpfen

Ein bestehendes Feature des CMS ist die einfach Verlinkung auf einen anderen Beitrag über das Pseudotag <link …>, das eine Text-Id eines anderen Beitrags erwartet und bei der Anzeige daraus einen Link generiert, der die Headline des verknüpften Beitrags zeigt. Das Ermitteln der thematisch nahe stehenden weiteren Beiträge wurde bislang vom System nicht unterstützt.

sajax4-144

Um das zu ändern, gibt es nun einen Button, der eine Select-Box mit den 10 thematisch ähnlichsten Texten füllt. Die Ermittlung der Ähnlichkeit erfolgt über eine Abfrage die die Texte mit der besten Stichwort-Übereinstimmung ermittelt. Dazu wird die m:n-Tabelle, die einem Text ein Stichwort zuweist, mit sich selbst verknüpft (Auto-Join), um alle Stichwort-Verweise zu ermitteln, die andere Texte genauso wie der aktuellen Text haben. Über eine Gruppierung nach Text-Id und Sortierung nach absteigender Häufigkeit werden die nächsten Verwandten des Textes ermittelt. Dies ist der einzige Datenbank-Zugriff, der im Skript belassen wurde, weil er nicht trivial ist und Ihnen vielleicht für eigene Probleme weiterhelfen kann.

Aus der gefüllten Select-Box kann der Anwender nun die Beiträge identifizieren, die er passend findet und sie markieren. Der Knopf Als Link übernehmen trägt sie dann an der aktuellen Cursorposition im Body-Feld ein. Das muss leider für IE und die Mozilla-Fraktion getrennt erfolgen, da die Methoden zum Behandeln des Inneren eines <textarea> völlig unterschiedlich arbeiten.

 

Sajax hat dazugelernt

Das Framework Sajax eignet sich für PHP und andere Sprachen. Es erleichtert die Arbeit, weil es Dinge abnimmt, die man zum Beispiel beim Einsatz von Json (siehe ) selbst erledigen muss. So etwa das Zusammenbauen der Parameter, die zum Server übertragen werden oder die Auswertung von Rückgabeparametern des xmlhttprequest-Objekts, das den Datentransport erledigt. Dabei ist Sajax leicht verständlich und zwingt dem Programmierer kein enges Korsett auf, wie andere Ajax-Frameworks, die eigene Funktionalitäten nur als Ableitung einer vorhandenen Ajax-Klasse ermöglichen.

Ein Problem früherer Sajax-Versionen war, dass als Rückgabeparameter von PHP-Funktionen generell nur ein einziger String vorgesehen war. Die neuere Version Sajax 0.12 behebt dieses Manko. Ganz ähnlich wie bei Json werden komplexe Datentypen in eine Javascript-Notation gepackt. Anders als dort, müssen Sie die Auswertung per eval() nicht selbst vornehmen. Das erledigt das Framework für Sie.

Eine Besonderheit dabei sind PHP-Arrays. Denn die werden nach Javascript als Objekte transferiert. Angenommen, ein Rückgabewert ist zum Beispiel in PHP als array(“obst”=>”banane”,”farbe”=>”blau”) definiert und Sie haben in Ihrer Callback-Funktion den Rpckgabeparameter oRes genannt. Dann erhalten Sie in der Javascript-Funktion, die sich um die Auswertung der aufgerufenen PHP-Funktionen kümmert, den Farbwert beispielsweise durch oRes.farbe.

Einschränkungen der Beispiellösung

Ein typisches Problem bei der Umstellung einer existierenden Anwendung haben wir im hier vorgestellten Beispiel unterschlagen: Die Neuaufnahme. Denn alle Funktionalitäten gehen davon aus, dass es bereits eine ID für den aktuellen Text gibt. Am einfachsten würde man das so lösen, dass der Benutzer einmalig manuell eine Speicherung auslösen muss. Erst danach machen Aktionen wie die Zuordnung von Stichwörtern über einzelne Datenbank-Aktionen Sinn.

Im Beispielskript ist außerdem die Stichwortanzeige als statischer HTML-Text ausgeführt. In einer echten Anwendung müssen Sie die Erzeugung der Stichwortliste stattdessen dynamisch via PHP lösen. Dazu verknüpfen Sie in einer LEFT JOIN-Abfrage alle vorhandenen Stichwörter mit den Stichwort-Einträgen des aktuellen Textes. Je nachdem, ob beim Stichwort die Id des Textes erscheint oder nicht, sollte Ihr PHP dann den Listeneintrag die CSS-Klasse set_on oder set_off zuweisen, damit der Benutzer die aktuellen Stichwörter erkennen kann.

Diskussionswürdig ist sicher auch die zwangsweise Autospeicherung. Die würde man sicher besser optional realisieren und dazu Checkbox einführen, die beim Aktivieren über eine weitere Javascript-Funktion den Intervall-Timer setzt und beim Entfernen des Häkchens wieder löscht.

Bessere Übersicht mit zweigeteiltem Sajax-Code

Als Standard erwartet ein Sajax-Programm den PHP-Code, der die Javascript-Anfragen beantwortet im selben Skript, das auch den HTML-Code erzeugt. In diesem Beispiel wäre das allerdings ziemlich unübersichtlich geworden, weil viele Funktionen beteiligt sind. Über die die globale Sajax-Variable $sajax_remote_uri wird darum im Client-Teil der Name der Seite definiert, die die Anfragen beantwortet. Angenehm dabei ist, dass hier keine einzige Zeile drin steckt, die HTML erzeugt. Neben den fremdaufgerufenen Funktionen finden sich darum hier lediglich Befehle, um die Datenverbindung aufzubauen.

Mag das ganze trotz aller Übersichtlichkeit nicht klappen, hilft vielleicht die globale Variable $sajax_debug_mode weiter. Wird Sie auf 1 gesetzt, gibt Sajax hilfreiche Alert-Meldungen aus, die klar machen, was gerade passiert.

Installation von Sajax

Holen Sie sich von www.modernmethod.com/sajax die aktuelle Version des Frameworks. Entpacken Sie aus dem Unterverzeichnis /php die Datei Sajax.php und sorgen Sie dafür, dass sie in ihre Skripts per include() geladen wird.

 

Skript edit_text.php

<?php

require(“Sajax.php”);

sajax_init();

$sajax_debug_mode = 0;

$sajax_request_type=”POST”;

$sajax_remote_uri = “client_request.php”;

sajax_export(“save_text”,”kw_on”,”kw_off”,”suggest_similar”);

?>

<html><head>

<title>Sajax</title>

<link rel=”stylesheet” type=”text/css” href=”style.css” />

<script type=”text/javascript”>

var timerAutosave= window.setInterval(“save_text(0)”, 60 * 1000);

function save_text(bLeaveForm){

with(document.editor){

x_save_text(bLeaveForm, textid.value, head.value,

body.value, save_text_cb);

}

}

function save_text_cb(oRes){

document.getElementById(‘savestate’).innerHTML = oRes.savestate;

if(oRes.leaveform == 1)

window.location.href=”/main.php”;

}

function click_li(o){

var kw_nr = o.id.substring(3);

if (o.className==”set_on”)

x_kw_off(document.editor.textid.value,kw_nr,kw_off_cb);

else

x_kw_on(document.editor.textid.value,kw_nr,kw_on_cb);

}

function kw_on_cb(oRes){

var kw_id = “kw_” + oRes.kw_nr;

document.getElementById(kw_id).className = “set_on”;

}

function kw_off_cb(oRes){

var kw_id = “kw_” + oRes.kw_nr;

document.getElementById(kw_id).className = “set_off”;

}

function suggest_similar() {

x_suggest_similar(document.editor.textid.value,suggest_similar_cb);

}

function suggest_similar_cb(oRes){

var sugg_field = document.editor.similarities;

for (i=0;i<10;i++){

sugg_field.options[i] = new Option(oRes[i].thead,oRes[i].tid);

}

}

function insert_links(){

var newlinks=””;

var opts = document.editor.similarities.options;

for (i=0;i<opts.length;i++){

if (opts[i].selected)

newlinks += “<link ” + opts[i].value +”>n”;

}

insertAtCursor(document.editor.body,newlinks);

}

function insertAtCursor(oArea, sInsText) {

var bBrowserOk=false;

if (navigator.appName == “Netscape” && ( navigator.appVersion.charAt(0) >= “5” )) {

// Netscape ab 5.0

bBrowserOk=true;

oArea.focus();

nCurpos = oArea.selectionStart;

sTextPre = oArea.value.substr(0,nCurpos);

sTextPost = oArea.value.substr(nCurpos,oArea.value.length-nCurpos);

oArea.value = sTextPre+ sInsText +sTextPost;

}

if ( navigator.appName == “Microsoft Internet Explorer” ) {

// IE unter Windows / Linux

bBrowserOk=true;

oArea.focus();

TextRange = document.selection.createRange();

TextRange.text = sInsText;

}

if (!bBrowserOk)

alert (“Das geht leider mit Ihrem Browser nicht”);

}

<? sajax_show_javascript();  ?>

</script>

</head>

<body >

<form name=”editor” >

<fieldset><legend>Text ändern</legend>

<p id=”savestate”>ungespeichert</p>

<p>Text-Id <input name=”textid” value=”3154″ readonly size=”4″/></p>

<p>Headline <input name=”head”/ size=50></p>

<p>Text <textarea name=”body” cols=”50″ rows=”8″></textarea></p>

<p>Verwandte Texte

<select name=”similarities” size=5 id=”id_person” multiple></select></p>

<p>

<input type=”button” onclick=”suggest_similar()” value=”Verwandte Texte suchen”/>

<input type=”button” onclick=”insert_links()” value=”Als Link übernehmen”/>

</p>

<p>Stichwörter</p>

<ul class=”sortable boxy” style=”margin-left: 1em;” >

<li class=”set_off” id=”kw_1″ onclick=”click_li(this)”>Computer</li>

<li class=”set_off” id=”kw_2″ onclick=”click_li(this)”>Haushalt</li>

<li class=”set_off” id=”kw_3″ onclick=”click_li(this)”>Video</li>

<li class=”set_off” id=”kw_4″ onclick=”click_li(this)”>Sparen</li>

</ul>

<input type=”button” onclick=”save_text(1)” value=”Speichern & Ende”/>

</fieldset>

</form>

</body>

</html>

 

Skript client_request.php

<?php

require(“Sajax.php”);

mysql_connect(‘localhost’,’root’,’pw’);

mysql_select_db(‘textsys’);

sajax_init();

sajax_export(“save_text”,”kw_on”,”kw_off”,”suggest_similar”);

sajax_handle_client_request();

function save_text($bLeaveForm,$nTextId,$sHead,$sBody){

// …Datenbank-Update durchführen…

$arrReturn=array();

$arrReturn[‘leaveform’] = $bLeaveForm;

$arrReturn[‘savestate’] = “Autospeicherung ” . date(“H:m:s”);

return ($arrReturn);

}

function kw_on($nTextId,$nKeywordNr){

// …in DB Stichwort dem Text zuordnen…

return(array(“kw_nr”=>$nKeywordNr));

}

function kw_off($nTextId,$nKeywordNr){

// …in DB Stichwort dem Text wegnehmen…

return(array(“kw_nr”=>$nKeywordNr));

}

function suggest_similar($nTextId){

$strSql = “SELECT count(*) , t2kb.tid, t.thead

FROM tip2key AS t2ka, tip2key AS t2kb,tips t

WHERE t2ka.tid =$nTextId

AND t2ka.kid = t2kb.kid

AND t2kb.tid <> t2ka.tid

AND t2kb.tid = t.tid

GROUP BY t2kb.tid

ORDER BY 1 DESC, RAND()

LIMIT 10”;

$result = mysql_query($strSql);

$arrReturn = array();

while ($row = mysql_fetch_assoc($result)) {

$arrReturn[]=array(“thead”=>$row[‘thead’],”tid”=>$row[‘tid’]);

}

return($arrReturn);

}

mysql_close();

?>

Ähnliche Beiträge