Archiv der Kategorie: AJAX

Drupal Bewertungsformular mit AJAX

Was in WordPress so schön funktioniert hat, sollte doch auch in Drupal zu machen sein: ein Bewertungsformular, das sich beim Abschicken einer neuen Bewertung selbst updated, eine genaue Beschreibung dazu findet man in diesem Artikel im Blog zum schwarzen Pinguin. Man kann Noten von sehr gut bis ungenügend vergeben, und es gibt einen Button zum Abschicken der Bewertung. Dazu werden die evtl. bereits vorhandenen Bewertungen und die Durchschnittsnote angezeigt, aussehen tut das Ganze so:

bewertungformular1
bewertungformular1

Voraussetzungen in Drupal

Ich werde dieses Formular nur für einen ausgesuchten Inhaltstypen anlegen, ich nehm mal die Artikel. Dazu brauchen wir einen Template Override – das hört sich schlimmer an als es ist.

Um die Bildschirmanzeige für alle Inhalte vom Typ Artikel zu ändern, klemmt man sich aus dem Verzeichnis ../themes/[dein theme]/templates die node.tpl.php und macht eine Kopie davon. Diese benennt man um in node–article.tpl.php (doppelter Bindestrich!). Dann sucht man sich die richtige Stelle nach dem Content aus, das ist in meinem Fall nach dem Block:

<div class="content clearfix"<?php print $content_attributes; ?>>
<?php
// We hide the comments and links now so that we can render them later.
hide($content['comments']);
hide($content['links']);
print render($content);
?>
</div>

Da ich das Formular nur in der Einzelansicht anzeigen möchte, füge ich hier eine if-Abfrage ein, die sieht erstmal so aus:

<?php
if ($view_mode == 'full'){

echo "<h1>Hier kommt das Formular hin</h1>";
}
?>

Cache leeren nicht vergessen, und nachgucken ob der Text an der richtigen Stelle angezeigt wird. Dann kann man hier den Funktionsaufruf reinpacken:

if ($view_mode == 'full'){

el_bew_ajax();

}

Ein eigenes Modul für das Formular

Das Formular selber packe ich in ein selbstgebautes Modul, zur Erstellung eines einfachen Moduls in Drupal kann man in diesem Artikel nachschlagen. Zuerst kommt mal das HTML für die Anzeige des Formulars rein, das wird in eine Funktion gesteckt:

function el_bew_ajax(){

echo '<div style="border:1px dotted #000; text-align:center; padding:10px;">';
echo '<h4>Dir gefällt dieser Beitrag?</h4>';
echo '<p>Dann bewerte ihn!</p>';
echo '</div>';

echo " <form action = '' method='post'>";

echo "<fieldset>
<div id = 'noten' class = 'noten' style = 'border:2px solid blue; padding :4px;'>
<input type='radio' name='note' value='1'>sehr gut
<input type='radio' name='note' value='2'>gut
<input type='radio' name='note' value='3'>befriedigend<br>
<input type='radio' name='note' value='4'>ausreichend
<input type='radio' name='note' value='5'>mangelhaft
<input type='radio' name='note' value='6'>ungenügend

<input type ='button' name = 'absenden' class = 'bew-button' value = 'abschicken' onclick='el_bew_ajax()'/>
</div>
</fieldset>";
echo "</form>";


}//ende function el_bew_ajax

Das habe ich 1:1 aus der oben zitierten WordPress-Lösung kopiert, funktioniert hier genauso. Das Javascript wird bei Klick auf den ‚absenden‘-Button aufgerufen, dazu gleich mehr.

Eine Tabelle für die Bewertungen

Auch diese sieht genau wie in der WordPress-Lösung aus und enthält drei Felder, eine Autowert-ID, ein Int für die nid und ein Int für die Note:

drupal_bewertungen
drupal_bewertungen

Die Logik für die Anzeige der bereits vorhandenen Bewertungen

Diese musste ich nur ein wenig auf Drupal-Verhältnisse anpassen, die Datenbankabfrage funktioniert hier mit db_query. Die aktuelle Beitragsid ist ein bisschen sperrig abzufragen, da suche ich noch eine hübschere Lösung:

//nid des aktuellen Beitrags bestimmen und in span schreiben
$nid = 0;
if (arg(0) == 'node' && is_numeric(arg(1))) {
$nid = arg(1);
}
echo 'NID dieses Beitrags: <span id = "beitragsid">'.$nid.'</span>';

//******aktuelle Benotungen abholen und Durchschnitt berechnen


$result = db_query('SELECT * FROM {bewertungen} WHERE nid = '.$nid.'');
$gefunden = $result->rowCount();


$durchschnitt = 0;
foreach ($result as $record) {
$durchschnitt = $durchschnitt + $record->note;

}
if ($gefunden != 0){ 
$durchschnitt = $durchschnitt / $gefunden;
}
echo "<span id = 'ausgabe'>".$gefunden." Bewertungen gefunden, Durchschnitt: ".$durchschnitt."<span>";

Das Javascript

…wird  am Anfang der .module-Datei eingebunden, und zwar so:

function ajax_el_init() { 

drupal_add_js(drupal_get_path('module', 'ajax_el') . '/test_ajax_el.js');

}

Die js-Datei liegt bei mir einfach im selben Verzeichnis wie das Modul. Sie sieht wird in einen Wrapper eingebunden, damit man $ für jQuery verwenden kann:

(function ($) {
function el_bew_ajax(){

//Hier kommt der Code

}//end function el_bew_ajax
})(jQuery);

Die ganze Funktion für den Ajax-Call sieht folgendermassen aus:

(function ($) {
function el_bew_ajax(){


//Beitragsid aus span holen
var akt_id = document.getElementById("beitragsid").innerHTML;

//Note aus den Radiobuttons holen
var akt_note = $("input:radio[name=note]:checked").val();

//Debug-Ausgabe
//alert(akt_note + ' ' + akt_id);

//Check ob keine Note angewählt, Abbruch
if (typeof akt_note === 'undefined'){
alert('Bitte eine Note wählen!');
return;}

//***********************ajax-call
if (window.XMLHttpRequest)
{
// AJAX nutzen mit IE7+, Chrome, Firefox, Safari, Opera
xmlhttp=new XMLHttpRequest();
}
else
{
// AJAX mit IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("ausgabe").innerHTML = xmlhttp.responseText;

}
}
xmlhttp.open("GET","../sites/all/modules/ajax_el/callback_ajax_el.php?id="+akt_id+"&note="+akt_note,true);

xmlhttp.send();



//**********************end ajax call
}//end function el_bew_ajax
})(jQuery);

Ich hole mir am Anfang die nid des Beitrags und die aktuelle Note aus dem Formular und gebe eine Meldung aus falls keine Benotung angewählt wurde.

Die Funktionalität für den Ajax-Call ist die selbe wie die, die wir schon für den Ajax-Autocomplete in diesem Beitrag verwendet haben, das drösel ich hier jetzt nicht nochmal auf.  Der einzige Unterschied ist, dass hier am Ende zwei Parameter statt nur einem an die Callbackfunktion übergeben werden.

Die Callback-Funktion

ist auch recht einfach. Ich hole mir die zwei Werte für die Note und die nid des Beitrags, mache einen Insert auf die Tabelle bewertungen und berechne den neuen Durchschnitt sowie die Anzahl der Bewertungen. Zurückgegeben werden die neuen Werte, die kriegt unser Script als  response und schreibt sie in die div ‚ausgabe‘ im Formular.

$nid = '';
if ( isset($_GET['id']) )
{
$nid = $_GET['id'];
}
$note = '';
if ( isset($_GET['note']) )
{
$note = $_GET['note'];
}

$pdo = new PDO('mysql:host=localhost;dbname=druneu', '******', '*******');

//neue Note erstmal einfügen
$sql = "INSERT INTO bewertungen (nid, note) VALUES (".$nid.",".$note.")";
$neu = $pdo->query($sql);

//Suche nach aktueller nid
$sql = "SELECT * FROM bewertungen where nid = ".$nid."";
$records = $pdo->query($sql);
$gefunden = $records->rowcount();

$durchschnitt = 0;
foreach ($records as $row) {

$durchschnitt = $durchschnitt + $row['note'];
}
if ($gefunden != 0){ 
$durchschnitt = $durchschnitt / $gefunden;
}
echo $gefunden." Bewertungen vorhanden, Durchschnittsnote = ".$durchschnitt;

Das war schon der ganze Zauber! Man kann jetzt noch die Durchschnittsnoten runden und per switch case die entsprechenden Bezeichnungen vergeben (1=sehr gut… 6=ungenügend), aber das ist Feintuning, das spare ich mir jetzt.

Aussehen tut das Ganze jetzt so:

bewertungsformular2
bewertungsformular2

Tipp: damit man sich beim Testen nicht schwarz ärgert wenn was nicht gleich funktioniert: Cache leeren nach jeder Script-Änderung!

Rezeptsuche mit AJAX Autocomplete 2: das Javascript und die PHP-Datei

Das Javascript

Wir haben ja im vorigen Beitrag unsere Javascriptdatei rezepte_suche.js geladen, jetzt stelle ich sie hier mal in aller Kürze vor. Die ist nicht auf meinem Mist gewachsen, das ist eine Standardprozedur für einen AJAX-Call die ich mir zusammengegooglet und nach meinen Bedürfnissen angepaßt habe. Sie enthält genau eine Funktion, nämlich die rezepte_suche mit dem Parameter inhalt, in dem steckt der vom Benutzer eingegebene Inhalt des Textfeldes aus dem Formular:

function rezepte_suche(inhalt)
{
 if (inhalt=="")
 {
  return;
 }
 if (window.XMLHttpRequest)
 {
  // AJAX nutzen mit IE7+, Chrome, Firefox, Safari, Opera
  xmlhttp=new XMLHttpRequest();
 }
 else
 {
  // AJAX mit IE6, IE5
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
 }
 xmlhttp.onreadystatechange=function()
 {
  if (xmlhttp.readyState==4 && xmlhttp.status==200)
  {
   
   document.getElementById("liste").innerHTML=xmlhttp.responseText;
  }
 }
 xmlhttp.open("GET","http://localhost/drumenus/sites/all/modules/autocom/rezeptesuche.php?q="+inhalt,true);

 xmlhttp.send();
}

Was passiert hier? Zuerst wird abgefragt, ob inhalt leer ist und in dem Fall nur ein return ausgelöst.

Wenn inhalt nicht leer ist, greifen zwei IF-Abfragen, die Browserabhängig die Variable xmlhttp unterschiedlich belegen. Wir schauen mal nur den ersten Zweig an:

xmlhttp=new XMLHttpRequest();

Hier wird ein neues Objekt vom Typ XMLHttpRequest angelegt, das wird im Folgenden weiterverarbeitet. Wer dazu Genaueres wissen möchte dem sei diese Seite der wikibooks empfohlen.

Wenn sich am Status des Formularfeldes etwas ändert, feuert folgende Funktion:

xmlhttp.onreadystatechange=function()
 {
  if (xmlhttp.readyState==4 && xmlhttp.status==200)
  {
   
   document.getElementById("liste").innerHTML=xmlhttp.responseText;
  }
 }

Die div mit der id liste kriegt einen neuen Inhalt, nämlich den Rückgabewert des Requests. Wir der aussieht, wird in unserer PHP-Datei geregelt, dazu gleich mehr.

Wichtig ist jetzt noch der open-Aufruf:

xmlhttp.open("GET","http://localhost/drumenus/sites/all/modules/autocom/rezeptesuche.php?q="+inhalt,true);

Ich hab hier den Pfad zur PHP-Datei noch fest verdrahtet, das läßt sich sicher auch eleganter lösen, aber für Testzwecke tuts das. Die PHP-Datei kriegt als Parameter unseren inhalt mit, das ist immer noch der Inhalt des Formularfeldes so wie ihn der Benutzer eingegeben hat. Fehlt nur noch der Auslöser:

xmlhttp.send();

Und Schuß! Das setzt den Request ab.

Die aufgrufene PHP-Datei

Die benimmt sich nicht anders, als wenn sie von einem Formular aus aufgerufen würde, und sie übernimmt den Parameter inhalt in der Variablen q. Ansonsten ist sie recht übersichtlich:

<?php

$rezept = '';
if ( isset($_GET['q']) )
{
  $rezept = $_GET['q'];
}

$pdo = new PDO('mysql:host=localhost;dbname=drumenus', '*****', '*****');

//Suche nach enthaltenem String %suchstring%
$sql = "SELECT * FROM node where title like '%".$rezept."%' and type like 'rezept' order by title";
$records = $pdo->query($sql);

foreach ($records as $row) {
        
        echo "<option>".$row['title']." #".$row['nid']."</option><br>";
        
    }
?>

Ich hole mir erstmal den Inhalt von q auf die Variable $rezept. Dann verbinde ich mich mit der Drupal-Datenbank und hole mir aus der node-Tabelle mit dem Select alle Datensätze der Rezepte (type like ‚rezept‘), die den Inhalt von $rezept im Titel enthalten. Innerhalb der Foreach-Schleife wirds dann nochmal interessant, ich klemme vor und nach dem Titel des Rezeptes die <option>-Tags dran, die brauchen wir nachher im Formular für die Datalist. Ausserdem nehme ich noch die nid mit, die brauchen wir dann für die Ausgabe des Rezeptes. Das wars dann aber auch schon.

Das Formular ist jetzt scharfgeschaltet

Wenn der Benutzer jetzt zum Beispiel salat in das Textfeld eingibt, erhält er eine Liste aller Rezepttitel, in denen der String salat vorkommt:

salat
salat

Jetzt muss nur noch etwas passieren, wenn er einen der Einträge auswählt und auf Return drückt. Ich hab das so gelöst: in der autocom.module kommt nach dem Formular noch folgender Code:

if (isset($_POST['ausgesucht'])){
    
    echo "Vielen Dank! Ihre Auswahl: <h2>".$_POST['ausgesucht']."</h2><br>";

    $text = strstr($_POST['ausgesucht'], '#');
    $text=substr($text,1); 
    
    $options = array('absolute' => TRUE);
    $nid = $text; // Node ID
    $url = url('node/' . $nid, $options);
    
    echo "<h2><a href = ".$url.">Link zum Rezept</a></h2>";

    } //ende von if isset

Wenn der Benutzer einen Eintrag ausgewählt hat (If (isset…)), zerlege ich mir die Auswahl und hole mir nur die nid nach dem #. Zu der wiederum hole ich mir die URL des betreffenden Nodes, und gebe damit einen Link zum Rezept aus. Das wars! Aussehen tut das Ganze so:

link_zum_rezept
link_zum_rezept

Ich hab hier jetzt mal mit dem Link gearbeitet, man könnte hier natürlich auch mit der nid in die Tabelle field_data_body gehen und sich den body_value ausgeben lassen, das wäre der komplette Text des ausgewählten Rezeptes, aber das ist reine Geschmackssache.

Drupal-Puristen werden mich jetzt wahrscheinlich steinigen, weil man „das so nicht machen kann“, aber der Autocomplete funktioniert jedenfalls ohne allzu grossen Aufwand. Es gibt sicher hier und da noch ein paar Optimierungspotentiale, was man unbedingt noch angehen sollte wäre der fest verdrahtete Pfad zu der PHP-Datei in der rezepte_suche.js, das ist sicher nicht so besonders gut gelöst, aber für Demozwecke tuts das, finde ich. mir hats Spaß gemacht, und das Ergebnis ist ganz ansehnlich.

Rezeptsuche in Drupal mit AJAX Autocomplete

Einleitung

Wahrscheinlich werden da jetzt alle Drupal-Puristen Zetermordio schreien, dass man das so nicht machen kann, aber wenns funktioniert… ich bin da nicht so pingelig.

Das Vorhaben: Suche mit Autocomplete

Ich hab ja über 300 Rezepte in meinem Drupal-Kochbuch, und was in WordPress funktioniert hat, sollte auch in Drupal machbar sein: eine Suche mit Autocomplete bzw. auf Deutsch Auto-Vervollständigung. Das heisst, wenn der Benutzer in das Suchfeld einige Buchstaben eingibt, sollen per AJAX gematchte Einträge als Vorschläge in der Liste auftauchen. Dabei verwende ich die HTML5-Datalist, auch wenn die browserabhängig unterschiedliche Ergebnisse liefert, darüber habe ich mich im letzten Beitrag näher ausgelassen. Macht mir aber jetzt nicht wirklich viel aus, man muss es halt nur wissen.

Wir bauen uns ein Modul

Wir packen die ganze Sache natürlich in ein Modul, und das kriegt einen eigenen Menüeintrag, so gehts am Einfachsten. Zur Modulerstellung in Drupal sag ich jetzt nicht mehr viel, das hatten wir schon ausführlich in diesem Artikel und anderswo.

Das Modul heisst bei mir autocom, dazu brauchts erstmal die autocom.info, die sieht bei mir so aus:

name = Autocom
description = Dieses Modul soll Autocompleten
core = 7.x
package = "MyFunctions"
version = "7.1-1.1"

Und dann brauchen wir natürlich noch die autocom.module, zu der ist etwas mehr zu sagen, in der laden wir uns nämlich erstmal die für AJAX benötigte Javascript-Datei. Das passiert ganz oben im File:

function autocom_init() {  
     
     drupal_add_js(drupal_get_path('module', 'autocom') . '/rezepte_suche.js');
    
    }

Mit der Syntax [modulname]_init wird Drupal angewiesen, diese Anweisung beim Laden des Moduls auszuführen. drupal_add_js() ist für das Laden von Javascript zuständig, das drupal_get_path( ‚module‘, ‚[modulname]‘) holt uns den Pfad zum angegebenen Modul, und an den hängen wir unsere js-Datei mit dran, weil die mit im Modulverzeichnis liegt. Was die Scriptdatei macht, dazu später mehr.

Die Funktion für das Suchformular kommt als nächstes in die autocom.module, die sieht erstmal so aus:

function evi_auto(){
      
echo "<h1>Beispiel Rezeptsuche mit AJAX</h1>";
echo "<form action='#' method='post'>";
echo "<input type='text' size='80' name = 'ausgesucht' value='' autocomplete='off' onkeyup='rezepte_suche(this.value)' list = 'suchliste'>";

echo "<datalist id='suchliste'>";

 echo "<div id =liste>";
 echo "</div>";

 echo "</datalist>";

echo "</form>";
}

Egentlich ein ganz normales Formular mit einem Eingabefeld vom Typ Text. Aber hier passieren noch drei eminent wichtige Dinge:

  1. onkeyup = ruft die js-Funktion rezepte_suche() auf, die steckt in der rezepte_suche.js, die wir vorher geladen haben. Die Funktion kriegt als Parameter this.value mit, also den aktuellen Wert des Textfeldes. Onkeyup feuert, wenn der Benutzer eine Taste gedrückt und wieder losgelassen hat.
  2. Der Parameter list = ‚suchliste‚ des Textfeldes definiert, aus welchem HTML-Element die Datenliste für das Dropdownfeld genommen werden soll, nämlich aus der Datalist mit der id = „suchliste“
  3. Innerhalb der Datalist steckt eine Div mit der id= ‚liste‘, in die schreibt nachher unser AJAX-Call sein Ergebnis, dazu gleich mehr.
  4. Wichtige Ergänzung: das Texteingabefeld sollte unbedingt den Parameter autocomplete = ‚off‘ mitkriegen,  sonst pfuscht der Browser mit Einträgen hinein, die er aus anderen Formularen gecached hat.

Wenn man das Modul angelegt und aktiviert hat, sollte das Formular jetzt etwa so aussehen:

formular_leer
formular_leer

Es passiert natürlich noch nichts wenn man da etwas eingibt, da uns die js-Funktion und die zugehörige PHP-Datei noch fehlen, aber dazu gibts einen neuen Beitrag.