Autocomplete mit Rails & Turbo

Aktualisierung: 22. Februar 2023 – Autocomplete für Kategorien ist inzwischen implementiert

Ruby on Rails LogoTurbo bietet die Technologie in einer Ruby on Rails Web-Anwendung, um dynamischer mit den Nutzern zu interagieren. Wie lässt sich die Eingabe des Autors eines Zitates damit vereinfachen? Zur Implementierung von Autocomplete gibt es viele Tutorials im Netz, z.B. das YouTube-Video Ruby on Rails #60 Hotwire Turbo Streams Autocomplete Search von Yaroslav Shmarov. Ich hatte ein wenig Mühe das gefundene in meine real-world Web-Anwendung zu übernehmen. Aber beim zweiten Versuch konnte ich die Teile besser zusammensetzen, und das möchte ich hier zeigen. Vielleicht hilft es anderen Anfängern ein wenig?

Screenshot-Ausschnitt zum AutocompleteIm zitat-service.de soll der Nachname des Autors automatisch ergänzt werden, wenn die Eingabe eindeutig ist und auch aus der Liste anklickbar sein. Die erste Umsetzung erfolgte mit einem zusätzlichen Formular für den Namen des Autors, Session-Variablen zur Synchronisierung mit dem Formular der übrigen Felder des Zitats. Das war überhaupt nicht rails-like, fehleranfällig und hatte das grundsätzliche Problem, dass bei der Auswahl aus der Liste, die bisher getätigten Eingaben bei dem Klick auf den Link verloren gingen.

Umsetzung im 2. Anlauf

Die Implementierung ist hier vereinfacht erläutert. Der komplette Quellcode und die Möglichkeit eine Docker Test-Instanz aufzusetzen sind unter github.com/muhme/quote zu finden.

Der Kommentar von Koen Handekyns in Syntax for returning multiple turbo streams ist ein hilfreicher Hinweis, wie man mit einem Render-Aufruf mehrere Turbo-Stream-Updates zur gleichen Zeit auslösen kann. Andere Teile des Puzzles sind:

  • in dem Partial _search_author.html.erb in einem Turbo-Frame gibt es das hidden field quotation[author_id] für die ID des Autors Quotation.author_id
  • dieses Partial enthält auch das sichtbare Eingabefeld author, in dem die Anfangsbuchstaben des Autorennamens eingegeben werden und in dem dann der gewählte Autor angezeigt wird
  • durch oninput: "this.form.requestSubmit()" wird mit der Eingabe jedes Buchstabens ein HTML-Event ausgelöst und ein Turbo-Stream-Request zum Controller geschickt
  • mit dem zweiten Partial _search_author_results.html.erb wird div#authors_list mit einer Liste von bis zu zehn gefunden Autoren gefüllt wird, oder bleibt leer und verschwindet damit
  • die Methoden create() und update() im QuotationsController verwenden den :commit Parameters des Submit-Buttons, um zu unterscheiden, ob der Request vom Abschicken des gesamten Formulars oder von der Eingabe von Text im Autorenfeld stammt
  • wenn durch die Eingabe der ersten Buchstaben exakt ein Autor gefunden wird, dann werden zwei Updates mit der Methode turbo_author() im QuotationsController ausgelöst:
    • im Partial _search_author.html.erb wird das hidden field quotation[author_id] gesetzt und und das Feld author mit Name, Vorname und Beschreibung gefüllt
    • dem zweiten Partial _search_author_results.html.erb wird eine leeres Array übergeben und somit verschwindet die Liste komplett
    • mit Hilfe von CSS wird eine 5-Sekunden-Animation von Grün ausgehend abgespielt, um zu verdeutlichen, dass die Eingabe erfolgreich war (dazu wird old_author_id mit der vorher im Formular gespeicherte Autoren-ID verglichen)
  • wenn es mehr als einen Autor für die ersten Buchstaben gibt, dann wird die Liste von bis zu 10 Autoren mit einem Turbo Stream Update aktualisiert
  • die Einträge in der Liste sind gewöhnliche HTML-Links <li>, die GET-Requests beim Anklicken eines Links rufen die Controller-Methode author_selected() auf, die dann wieder die zwei Updates, wie bei genau einem gefunden Autor auslöst
  • wird das Formular zum Anlegen eines Zitats abgeschickt, bevor genau ein Autor ausgewählt wurde, dann wird beim Anlegen eines neuen Zitates der Autor auf Unbekannt gesetzt und beim Ändern eines Zitates, ein Hinweis gegeben: „Autor … wurde nicht geändert”

Hier der entscheidene Abschnitt aus QuotationsController.update():

    if params[:commit]
      logger.debug { "update() quotation #{@quotation.inspect}" }
      if @quotation.update(quotation_params)
        hint = "Das Zitat wurde aktualisiert."
        # give hint, e.g. if author autocomplete field is not completed
        hint << " Der Autor „#{Author.find(@quotation.author_id).name}” wurde nicht geändert." if @authors.count != 1
        return redirect_to @quotation, notice: hint
      else
        render :edit, status: :unprocessable_entity
      end
    else
      turbo_author(old_author_id)
      turbo_category(nil, old_category_ids, Category.check(@quotation))
      render turbo_stream: turbo_stream_do_actions
    end


Mit dieser Lösung sind Session Variablen nicht mehr erforderlich. Der ausgewählte Autor steht im hidden field mit seiner ID. Der Code zum Erstellen und Aktualisieren unterscheidet sich nicht wesentlich. Ein Klick auf einen Link in der Liste der gefundenen Autoren löst zwar einen GET-Request aus. Aber innerhalb der Seite werden nur die beiden Turbo-Frames aktualisiert, die anderen Formulareingaben bleiben bestehen.

Aktualisierung

Inzwischen hat auch die Eingabe der Kategorien ein Autocomplete erhalten :)

Screenshot-Ausschnitt zum Autocomplete der Kategorien