Eigene Drupal Formulare themen mit hook_theme(), theme() und drupal_add_tabledrag()

Wie man eigene Formulare in Drupal erstellt, wurde in diesem Artikel hier gezeigt. Wenn man in einem Formular aber viele Daten abfragen möchte, kann es Sinn machen das Formular als eine Tabelle darzustellen. Wie man das macht, möchte ich am Beispiel eines Moduls zeigen, das ich vor kurzem programmiert hab: die Iconbar.

Die Iconbar soll kleine Icons, die vom Modul bereitgestellt werden, in einem Block in der Fußzeile der Seite anzeigen. Der Admin soll dann die Möglichkeit haben aus allen verfügbaren Icons auszuwählen, welche er anzeigen will, und mit welchen Seiten die Icons verlinkt sein sollen.

Dateistruktur

Zuerst brauchen wir eine neue Dateistruktur für unser neues Modul namens "iconbar". Die Dateistruktur sieht so aus (die Grafiken habe ich euch für Testzwecke an diesen Post angehängt):

  • /iconbar
    • /icons
      • footer_icon_facebook.png
      • footer_icon_meinvz.png
      • footer_icon_studivz.png
    • /includes
      • iconbar.admin.inc
    • iconbar.info
    • iconbar.install
    • iconbar.module

INFO-Datei

Die iconbar.info bauen wir uns schnell zusammen:

; $Id$
name = Icon Bar
description = Creates a set of icons in a block
core = 6.x

INSTALL-Datei

Die iconbar.install wird etwas umfangreicher. Da wir für jede Datei eigentlich nur Speichern müssen ob diese angezeigt werden sollen, in welcher Reihenfolge diese angezeigt werden sollen und wohin sie verlinken sollen, wäre es übertrieben einen eigenen Inhaltstypen dafür anzulegen. Stattdessen bauen wir uns einfach eine kleine eigene Tabelle in der Datenbank. Hier die Datei:

<?php
// $Id$

/**
 * @file
 * Performs install and uninstall tasks, such as creating content types and removing variables.
 */

/**
 * Implementation of hook_uninstall(). 
 *  
 * Removes the database table 
 */
function iconbar_uninstall() {
  drupal_uninstall_schema('iconbar');
}

/**
 * Implementation of hook_install().
 * 
 * Creates the necessary database table
 */
function iconbar_install() {
  drupal_install_schema('iconbar');
  
  drupal_set_message('Database table has been created');
  
  // Find available icons
  $typespath = drupal_get_path('module', 'iconbar') . '/icons';
  $dir_handle = @opendir($typespath) or die("Cannot open the path $typespath");

  // Loop through the files.
  while ($file = readdir($dir_handle)) {
    if ($file == '.' || $file == '..') {
      continue;
    }
    $sql = db_query("INSERT INTO {iconbar} (filename) VALUES ('%s')", $file);
  };
  
  drupal_set_message('Icons have been detected and stored in the database table. You can create links in the settings now.');
}

/**
 * Implementation of hook_schema().
 */
function iconbar_schema()
{
  $schema['iconbar'] = array(
    'description' => t('Stores icons and associated links for the footer bar.'),
    'fields' => array(
      'iid' => array(
        'type' => 'serial',
        'description' => t('The icon id.'),
      ),
      'filename' => array(
        'description' => t('The name of the icon file'),
        'type' => 'text',
        'not null' => TRUE,
        'size' => 'normal'
      ),
      'link' => array(
        'description' => t('The name of the associated link.'),
        'type' => 'text',
        'not null' => TRUE,
        'size' => 'normal'
      ),
      'published' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
        'description' => t('Whether the icon is displayed or not'),
      ),
      'weight' => array(
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
        'description' => t('The weight of the icon file. Determines the order in which these are displayed.'),
      ),
    ),
    'primary key' => array(
      'iid',
    ),
  );
  
  return $schema;
}

Was passiert hier? Schauen wir zuerst in iconbar_schema(). Hier wird die Datenbank beschrieben, die wir erstellen wollen. Der Sinn der Schema API ist es, Module allgemeingültig zu halten. Angenommen ihr beschreibt alles nach MySQL Vorgaben. Jemand der einen anderen Datenbanktypen verwendet (z.B. PostgreSQL) kann euer Modul unter Umständen dann nicht verwenden. Die Schema API beseitigt dieses Problem. Ihr braucht die Datenbank also nur so beschreiben, dass die Schema API sie versteht. Welche Datenbank dieser spezifischen Drupal Installation zugrunde liegt, braucht euch also gar nicht zu kümmern, denn die Schema API übersetzt eure Beschreibung so, dass die entsprechende Datenbank sie versteht.

Als erstes geht also die iconbar_install() hin und installiert die Datenbanktabelle mit drupal_install_schema('iconbar'). Mit dem gegensätzlichen Befehl drupal_uninstall_schema('iconbar') kann die Datenbanktabelle dann auch wieder entfernt werden, wie ihr in der iconbar_uninstall() sehen könnt.

Ist die Tabelle installiert geben wir eine kurze Statusmeldung aus und machen uns dann an die Arbeit, alle verfügbaren Icons in die neue Tabelle zu schreiben. Wir finden also zuerst den Ordner, wo alle Dateien liegen (in unserem Modul-Ordner im Unterordner "icons") und speichern dann alle gefundenen Dateinamen in der Datenbank in der Spalte "filename". Die Spalte iid steht auf 'serial' (das Äquivalent zu AUTOINCREMENT) und zählt daher automatisch hoch. Die anderen Spalten bleiben erstmal leer.

MODULE-Datei

Jetzt brauchen wir erstmal den Admin-Bereich, damit der Administrator die restlichen Infos in die Tabelle schreiben kann, so wie er sie benötigt. Dafür binden wir den hook_menu() ein.

<?php
// $Id$

/**
 * @file
 * Creates a set of icons in a block
 */

/**
 * Implementation of hook_menu().
 */
function iconbar_menu() {

  $items['admin/kyamstudios/iconbarlinks'] = array(
    'title' => 'Iconbar links',
    'description' => 'Assign links to selected icons.',
    'page callback' => 'drupal_get_form', // function called when path is requested
    'page arguments' => array('iconbar_link_settings'), // form id passed to the function
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'iconbar.admin.inc', // look for a function describing this form in this file
    'file path' => drupal_get_path('module', 'iconbar') . '/includes',
  );

  return $items;
}

Wie immer lagern wir die Administrationsformulare in eine eigene Datei aus, damit diese nicht bei jedem Modul-Aufruf vom System gelesen werden müssen und nur aufgerufen werden, wenn sie tatsächlich benötigt werden.

ADMIN.INC-Datei

function iconbar_link_settings($form_state) {
  
  $sql = db_query("SELECT iid, filename, link, published, weight FROM {iconbar} ORDER BY weight ASC");
  
  while($row = db_fetch_object($sql)) {
    
    $form['iconbarlinks'][$row->iid]['filename-'.$row->iid] = array(
      '#type' => 'value',
      '#value' => $row->filename,
    );

    $form['iconbarlinks'][$row->iid]['checked-'.$row->iid] = array(
      '#type' => 'checkbox',
      '#default_value' => $row->published,
      '#size' => 100,
    );
    
    $form['iconbarlinks'][$row->iid]['link-'.$row->iid] = array(
      '#type' => 'textfield',
      '#default_value' => $row->link,
    );
    
    $form['iconbarlinks'][$row->iid]['weight-'.$row->iid] = array(
      '#type'=>'weight',
      '#default_value'=>$row->weight,
      '#attributes' => array('class'=>'weight'),
    );
  }

  $form['iconbarlinks']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  
  return $form;

}

Als erstes lesen wir uns die Einträge aus der Datenbanktabelle aus und erstellen für jede Zeile einen eigenen Satz an Formularfeldern, jeweils mit den Schlüssen ['iconbarlinks']['datenbankschlüssel'][bezeichnung-datenbankschlüssel]. So ist es nachher einfacher im fertigen $form-Array unsere Einträge wiederzufinden und weiterzuverarbeiten.

Wir kümmern uns auch nicht groß um Title und Description Einträge, da wir das Formular nachher sowieso als Tabelle ausgeben wollen, und diese Informationen dann eher störend sind. Ihr könnt euch das Formular jetzt aber schon anschauen und prüfen ob ihr alles richtig gemacht habt.

Als nächstes brauchen wir eine Funktion zum Speichern der Formulareinträge:

function iconbar_link_settings_submit($form, &$form_state) {
  foreach ($form['iconbarlinks'] as $key=>$value) {
    if(is_numeric($key)) {
      db_query("UPDATE {iconbar} SET link = '%s', published = %d, weight = %d WHERE iid = %d", $value['link-'.$key]['#value'], $value['checked-'.$key]['#value'], $value['weight-'.$key]['#value'], $key);
    }
  }
}

Sieht etwas wirr aus, macht aber Sinn. Zwei Aspekte dieser Funktion benötigen eine Erklärung: Oben im Funktionsaufruf steht ein & Zeichen vor dem Parameter $form_state. Das ist kein Tippfehler, sondern Absicht. Wer schonmal in C oder C++ programmiert hat, kennt dieses Zeichen als Pointer, der nicht auf die Variable zeigt, sondern auf den Adressbereich der Variable. Nichts anderes ist das hier. Im Klartext: Setzt man das & vor die Variable wird nicht die Instanz dieser Variable angesprochen, sondern die eigentliche Variable selber. Alle Änderungen, die wir im Verlauf der Funktion an $form_state vornehmen, werden also direkt auch an unserer Ausgabe des Formulars vorgenommen. Man muss das veränderte Formular nicht erneut ausgeben. In diesem Fall benötigen wir diesen Pointer nicht, aber es macht Sinn sich die Verwendung der Pointer direkt anzugewöhnen. Bei einer Validierungs-Funktion z.B. wird dieser gern genutzt um die Eingaben zu prüfen bevor das Formular abgeschickt wird.

Der andere Punkt ist die Verwendung der is_numeric Funktion. Das hat den Hintergrund, dass wir in unserem Formular unter $form['iconbarlinks'] noch ganz andere Schlüssel haben, als nur die die wir brauchen, was ihr mit einem var_dump($form['iconbarlinks']) herausfinden könnt. Da wir unseren Feldern aber die Zeilen-ID der Datenbanktabelle als Schlüssel gegeben haben, können wir auf diese Weise perfekt nur die benötigten Felder auslesen.

hook_theme()

Jetzt funktioniert der Admin-Bereich unseres Moduls schon perfekt. Alle Änderungen werden gespeichert. Das dieses Formular bei zehn oder zwanzig Icons aber verdammt unübersichtlich werden kann, wollen wir das Formular jetzt als Tabelle darstellen. Hierfür brauchen wir hook_theme().

Wir gehen also zurück in die MODULE-Datei und erklären zuerst einmal dem System, dass wir in diesem Modul Elemente verwenden, die gestaltet werden sollen.

/**
 * Implementation of hook_theme().
 */
function iconbar_theme() {
	return array(
		'iconbar_link_settings' => array('arguments' => array('form' => NULL),),
	);
}

'iconbar_link_settings' ist hier natürlich die ID unseres Formulars, das wir gestalten wollen. In den arguments sagen wir noch, dass es sich dabei um ein Formular handelt.

Jetzt gehen wir zurück in die iconbar.admin.inc und erstellen eine Funktion in der wir die tatsächliche Gestaltung vornehmen. Die Funktion heisst immer theme_[formularid], also in unserem Fall theme_iconbar_link_settings()

function theme_iconbar_link_settings($form) {
  
  $table_rows = array();
  foreach($form['iconbarlinks'] as $id => $row){
    //we are only interested in numeric keys
    if (is_numeric($id)){
      $this_row = array();
      
      //Add the checkbox to the row
      $this_row[] = drupal_render($form['iconbarlinks'][$id]['checked-'.$id]);
      
      //Add the weight field to the row
      $this_row[] = drupal_render($form['iconbarlinks'][$id]['weight-'.$id]);
      
      //Add the filename to the row
      $this_row[] = $form['iconbarlinks'][$id]['filename-'.$id]['#value'];
      
      //Add the textfield to the row
      $this_row[] = drupal_render($form['iconbarlinks'][$id]['link-'.$id]);

      //Add the row to the array of rows
      $table_rows[] = array('data'=>$this_row, 'class'=>'draggable');
  
    }
  }

  //Make sure the header count matches the column count
  $header=array();
  $header[] = t('Display');
  $header[] = t('Weight');
  $header[] = t('Filename');
  $header[] = t('Associated Link (use http://)');

  $output = theme('table',$header,$table_rows,array('id'=>'iconbar-table'));
  $output .= drupal_render($form);

  // Call add_tabledrag to add and setup the JS for us
  drupal_add_tabledrag('iconbar-table', 'order', 'sibling', 'weight');      

  return $output;
}

Hier passiert so einiges. Diese Funktion nimmt sich unsere Daten für jede Zeile, schreibt diese in ein Array und übergibt das Array dann zusammen mit den Spaltenüberschriften und dem Hinweis, dass eine Tabelle erstellt werden soll, an die theme()-Funktion.

Gehen wir die Funktion einmal Schritt für Schritt durch. Zuerst erstelle ich ein neues Array $table_rows, in dem nachher alle meine Zeilen stehen sollen. Dieses Array übergeben wir dann nachher auch an die theme()-Funktion. Um jetzt jede Zeile einzeln zu erstellen, durchlaufen wir die Formabschnitte und schreiben für jeden Abschnitt die Werte in ein weiteres Array namens $this_row. Das erste Element meiner Zeile soll also die Checkbox sein, dann folgt die Angabe zur Reihenfolge (Weight), dann der Dateiname des Icons und dann das Textfeld, in das der Link geschrieben werden soll. Am Ende schreib ich dann das frische Array $this_row in mein Tabellen-Array $table_rows. Im neuen Durchlauf leere ich $this_row wieder und fülle es mit den Daten für die nächste Zeile.

Am Ende erstelle ich dann noch ein Array mit den Spaltenüberschriften ($header). Hier muss man darauf achten, dass genauso viele Überschriften existieren wie ich Spalten erzeugt habe, also in meinem Fall 4. Die Reihenfolge ist natürlich entsprechend zu erstellen. Jetzt muss ich diese Infos nur noch an die theme()-Funktion übergeben. Mit dem abschließenden drupal_render($form) stelle ich sicher, dass alle Formular-Elemente, die nicht schon bereits gerendert wurden auch noch gerendert werden (z.B. der Submit-Knopf).

drupal_add_tabledrag()

Hat man eine Tabelle in der die Reihenfolge der Elemente beeinflusst werden soll (also ein Weight-Feld), dann kann man drupal_add_tabledrag verwenden, um dem Benutzer die Möglichkeit zu geben die Elemente per Drag'n'Drop zu sortieren. Wie das funktioniert kann man z.B. in jeder Drupal-Installation in den Menü-Einstellungen beim Sortieren und Ordnen der Menüpunkte sehen.

Hat man sein Formular bereits als Tabelle "gethemed", dann geht das ganz einfach. Man muss nur sicherstellen, dass das Weight-Feld eine bestimmte Klasse hat (das haben wir in der iconbar.admin.inc Datei mit

'#attributes' => array('class'=>'weight'),

gemacht: Unser Weight-Feld hat also die Klasse "weight". Und dann muss man der gesamten Tabelle die im tabledrag-Stil dargestellt werden soll, noch eine ID geben. Das haben wir in der theme()-Funktion gemacht mit

$output = theme('table',$header,$table_rows,array('id'=>'iconbar-table'));

Hat man diese beiden Voraussetzungen erfüllt, reicht der einfache Befehl:

drupal_add_tabledrag('iconbar-table', 'order', 'sibling', 'weight');

Erster Parameter ist die ID der Tabelle und letzter Parameter ist die Klasse des Weight-Feldes. Ist alles richtig, dann wird das Weight-Feld jetzt zu einem kleinen per Drag'n'Drop steuerbaren Kreuz.

Kommentare

Good post. I study something more challenging on completely different blogs everyday. It is going to all the time be stimulating to learn content material from different writers and practice somewhat one thing from their store. I dakagkfcegbb