Für eine Website wollte ich ein Autocomplete-Inputfeld á la jQuery UI, allerdings mit noch ein paar erweiterten Eigenschaften, an denen ich mir einige Zeit lang die Zähne ausgebissen hatte.

Bei Eingabe einiger Zeichen sollte per AJAX eine Abfrage an den Webserver gestartet werden, das JSON-Ergebnis (wie auch im jQuery-UI-Beispiel) als Aufklappmenü dargestellt werden. Die fertige Lösung war für mich schon deshalb nicht ausreichend, da mein JSON-Ergebnis noch weitere Werte, als einfach nur Name und ID zurücklieferte, welche ich bei Auswahl im UI dann in weitere Formular-Felder schreiben wollte.
Falls sich das keiner vorstellen kann: Rechts ein kleines Diagramm dazu.

Probleme hatte ich allerdings dadurch, dass...

  1. ...ich bei der AJAX-Abfrage noch die Werte einiger anderen Formular-Felder mitgeben muss (und mir anfangs immer die Standard-Werte mitgegeben wurde, die das Formular zu dem Zeitpunkt hatte, als ich mein Autocomplete-Plugin initialisiert hatte)
  2. ...die Autocomplete-Felder flexibel zur Laufzeit entfernt und hinzugefügt werden können

Wer sich dafür interessiert: Im Folgenden die Lösungsansätze.

Erstes Problem

Zusätzlich zur im input-Feld eingegebenen Ortschaft sollte noch das in einem select ausgewählte Land eine Rolle bei der AJAX-Abfrage spielen.
Mein Formular-Input-Feld wurde wie folgt zum Autocomplete-Feld:

$(input).autocomplete({args: {'land': document.getElementById("mySelect").options[document.getElementById("mySelect").selectedIndex].value;} }, [...]);

Schnell merkte ich: Der Wert des Arguments land, welches ich mitgeben will, wird nicht dann geparst, wenn ich die AJAX-Abfrage absende, sondern gleich zu Anfang, wenn ich aus dem input-Feld mein Autocomplete-Feld mache.
Ist ja eigentlich auch logisch.

Nur, wie kann ich bei der AJAX-Abfrage den gerade ausgewählten Wert des select-Menüs mitgeben ohne dass ich das fest in meinen Plugin-Quellcode reincode? Ich will ja ein flexibles Autocomplete-Plugin haben.

Vielleicht findet jemand eine bessere Lösung, aber meine war, dem Argument land bei der Initialisierung nicht den Wert des select-Feldes mitzugeben, sondern das Ganze mit einer Funktion zu umschließen, die den Wert des selects zurück gibt:

$(input).autocomplete({args: {'land': function() {
     return document.getElementById("mySelect").options[document.getElementById("mySelect").selectedIndex].value;
     }
     } }, [...]);

Nächstes Problem

Mit

$(selector).autocomplete( [...] );
kann man das Plugin sehr einfach auf mehrere input-Felder anwenden, wenn aber zur Laufzeit weitere input-Felder dazu kommen, dann wird's kompliziert.
Da ziemlich viele Argumente im Aufruf mitgegeben werden, möchte ich das
$(selector).autocomplete( [...] );
nicht an zwei Stellen doppelt verwenden.

Meine Lösung hierfür: Den

$(selector).autocomplete( [...] );
-Aufruf in ein separates "Ãœber-Plugin" zu verschieben:

(function( $ ) {
        $.fn.speziellesautocomplete=function() {
                $(this).each(function() {
                        $(this).autocomplete({args: { [...] }, [...] }); // <-- Hier der sonst verwendete Aufruf
                        });
                return $(this);
                }
        })( jQuery );

Sowohl die ersten input-Felder, die sich gleich zu Anfang auf der Seite befinden, wie auch weitere input-Felder, die zur Laufzeit dazu kommen, kann ich nun mit einem einzigen

$(selector).speziellesautocomplete();
zum Autocomplete-Feld machen.

Das Plugin

Zu guter Letzt mein eigentliches Plugin.

Features:

  • Mit dem AJAX-Request zum Server wird nicht nur der in das Feld eingetippte Text mitgegeben; es können noch Werte weiterer Formularfelder oder sonstige Werte mitgegeben werden
  • Als JSON-Antwort können vom Server neben IDs und Namen noch weitere Informationen (Attribute) zurückgegeben werden, die dann bei Menü-Auswahl ebenfalls mitgegeben werden können
  • onReturn
    -Funktion (wenn die AJAX-Werte vom Server zurück kommen) frei modifizierbar
  • onSelect
    -Funktion (ein Eintrag wurde im UI vom Menü ausgewählt) frei modifizierbar

Dies ist sicher kein optimales, schön programmiertes jQuery-Plugin, aber zweckmäßig.
Im Folgenden werden noch die beiden Funktionen ajax und debouncer verwendet, um die Ajax-Abfrage abzusenden, respektive den Anfrage-Event erst abzufeuern, wenn für eine Weile keine Tastatureingabe mehr erfolgte.

// ajax({url: 'http://... .php', anfrage: 'sichern', args: {...} }, function(json) { // fertig... });
// Argument Anfrage wird ebenso wie andere Argumente als Post-Variable übergeben
// anfrage: 'sichern' => $_POST['anfrage'] = "sichern";
function ajax(args) {
  if (args.url == null)     { console.log("ajax: Keine URL \u00fcbergeben"); return false; }
  if (args.type == null)    { args.type = "POST"; }
  if (args.args == null)    { args.args = {}; }
  if (args.anfrage != null) { args.args.anfrage = args.anfrage; }
  var return_function = (ajax.arguments.length > 1 && typeof ajax.arguments[1] == "function") ? ajax.arguments[1] : function(ret) {};
  $.ajax({url: args.url, type: args.type, data: args.args}).done(function(ret) {
    if (args.noJSON)
      return_function(ret);
    else {
      var json = JSON.parse(ret.replace(/"/g, "").replace(/'/g, '"'));
      return_function(json);
      }
    }).fail(function() { alert("Bei der Anfrage ist ein Fehler aufgetreten."); });
  }

// debounce(function() { <auszuführende Funktion> }, <ausführen, wenn ? ms lang nicht getriggert> );
function debounce(fn, delay) {
  var timer = null;
  return function () {
    var context = this, args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
    };
  }

// $(selektor).autocomplete({args: { <POST-Argumente>},
//                           minChars: <Abfrage senden ab x eingegebenen Zeichen>,
//                           ajax_url: '<Abfrage senden an ... .php>',
//                           onReturn: <Alternative Funktion bei Serverantwort>,
//                           onSelect: <Alternative Funktion bei Menüauswahl>
//                           });
(function( $ ){
  $.fn.autocomplete=function(attr) {
    attr = $.extend({}, {args: {}, minChars: 3,ajax_url: '_bin/ajax.php', anfrage:'autocomplete_ajax',
               onReturn: function(json, menu, that, onSelectFunc) { _selectMenuAufbauen(json, menu, that, onSelectFunc); },
               onSelect: function(option, that) { _selectMenuSelected(option, that); }
               }, attr);
    $(this).each(function() {
      var that = $(this);
      $(this).bind("keyup", debounce(function(e) {
        if (e.keyCode <= 31) return false;
        if ($(this).val().length < attr.minChars) return false;
        attr.args.value = $(this).val();
        ajax({url: attr.ajax_url, anfrage: attr.anfrage, args: attr.args }, function(json) { attr.onReturn(json, menu, that, attr.onSelect); });
        e.cancelBubble=true;
        return false;
        }, 1000));
      var menu = document.createElement("div");
      menu.className = "autocomplete";
      menu.width = $(this).width()+"px";
      $(this).after($(menu));
      $(menu).position({"my": "left top", "at": "left bottom", "of": $(that)});
      $(this).bind("click", function(e) { window.setTimeout(function() {$(that).select(); }, 200); });
      $(this).bind("close", function(e) { $(that).next(".autocomplete").hide(); });
      $(document).bind("click", function() { $(that).next(".autocomplete").hide(); });
      });
    function _selectMenuAufbauen(json, menu, input, onSelectFunc) {
      if (json.result.length == 0 && $(menu).is(":visible")) { $(menu).hide(); return false; }
      if (json.result.length > 0) {
        $(menu).html("");
        for (var x=0; x<json.result.length; x++) {
          var item = document.createElement("div");
          $(item).addClass("item");
          $(item).html(json.result[x].name);
          for (var attr in json.result[x].attr)
            $(item).attr(attr, json.result[x].attr[attr]);
          $(item).on("click", function() { onSelectFunc($(this), input); });
          $(menu).append($(item));
          }
        $(menu).show();
        }
      }
    function _selectMenuSelected(option, input) {
      $(input).val($(option).html());
      }
    return $(this);
    };
  })( jQuery );

Beispiels-Verwendung:

(function( $ ){
$.fn.speziellesmenu=function() {
  $(this).each(function() {
    $(this).autocomplete({args: {'kunde':    function() { return document.rechnung.kunde.options[document.rechnung.kunde.selectedIndex].value; },
                                 'waehrung': function() { return document.rechnung.waehrung.options[document.rechnung.waehrung.selectedIndex].value; }
                                 },
                          ajax_url: 'ajax.php',
                          onSelect: function(option, input) { auswahl_getroffen(option, input); }
                          });
    });
  return $(this);
  }
function auswahl_getroffen(option, input) {
  $(input).val($(option).html());
  // Weitere Felder der Form mit Werten der ursprünglichen  AJAX-Antwort füllen
  var row = $(input).parents("tr");
  $(row).find(".beschreibung").val($(option).attr("beschreibung"));
  $(row).find(".epreis").val($(option).attr("preis"));
  $(row).find(".leistungid").val($(option).attr("leistungid"));
  $(row).find(".steuer").attr("checked", $(option).attr("steuer")==1);
  $(row).find(".epreis").trigger("keyup");
  }
})( jQuery );
$(".leistung").speziellesmenu();

Beispiels-JSON-Antwort von ajax.php:

{'result': [{'id':'12', 'name': 'Hans Meier', 'attr': {'tel': '0180965254', 'email': 'h.meier@mymail.com', 'strasse': 'mussterstrasse 12', 'plz': '12345', 'ort': 'musterstadt'} },
            {'id':'59', 'name': 'Ilse Meier', 'attr': {'tel': '01908754876', 'email': 'i.meier@yourmail.com', 'strasse': 'mussterstrasse 1b', 'plz': '12345', 'ort': 'musterstadt'} }
           ]}