Auto-Complete Association

Auto-Complete Association

Ein Auswahlfeld wird oft für die Festlegung einer "belongs_to"-Verknüpfung verwendet, aber Sie sollten auch in Betracht ziehen, ein Textfeld mit automatischer Vervollständigung zu verwenden. Hier verwende ich jQuery UI und zeige zwei verschiedene Lösungen: eine auf der Clientseite und eine auf der Serverseite.

Ein Produkt zu dieser App hinzuzufügen

Sagen Sie, wir haben eine einfache Anwendung, die eine Anzahl von Produkten verwaltet, von denen jedes zu einer Kategorie gehört. Um ein Produkt zu dieser App hinzuzufügen, haben wir ein Formular, das unten angezeigt wird, und das eine Dropdown-Liste für die Auswahl der Kategorie des neuen Produkts enthält.
$ rails g scaffold Product name price category:references
$ rails g scaffold Category name
$ rails db:migrate

// models/category.rb
class Category < ApplicationRecord
  has_many :products
end


1.png 616 KB


Dieser Ansatz funktioniert gut für eine begrenzte Anzahl von Kategorien, aber Dropdown-Listen können schnell umständlich werden, wenn es eine große Anzahl von Optionen zur Auswahl gibt. In dieser Episode werden wir dieses Dropdown durch ein Textfeld ersetzen, das automatisch vervollständigt wird, wenn ein Benutzer den Namen einer Kategorie eingibt.

Die Vorlage, die das Formular rendert, wird unten gezeigt. Sie verwendet "collection_select", um das Dropdown für die Kategorie-Verknüpfung zu erstellen.
//app/views/products/_form.html.erb

<%= form_with(model: product) do |form| %>
<% if product.errors.any? %>
<div style="color: red">
  <h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>

  <ul>
    <% product.errors.each do |error| %>
    <li><%= error.full_message %></li>
    <% end %>
  </ul>
</div>
<% end %>

<div>
  <%= form.label :name, style: "display: block" %>
  <%= form.text_field :name %>
</div>

<div>
  <%= form.label :price, style: "display: block" %>
  <%= form.text_field :price %>
</div>

<div>
  <%= form.label :category_id, style: "display: block" %>
  <%= form.collection_select :category_id, Category.order(:name), :id, :name, include_blank: true %>
</div>

<div>
  <%= form.submit %>
</div>
<% end %>
Wir werden das "collection_select" durch ein Textfeld ersetzen und es "category_name" nennen, da der Benutzer hier den Namen eingeben wird.
//app/views/products/_form.html.erb

<div class="field">
  <%= f.label :category_name %><br />
  <%= f.text_field :category_name %>
</div>
Wenn wir versuchen, das Formular jetzt neu zu laden, sehen wir eine Fehlermeldung, da das Produktmodell kein Attribut "category_name" hat. Um dies zu beheben, werden wir ein virtuelles Attribut erstellen.
//app/models/product.rb
class Product < ActiveRecord::Base
  belongs_to :category
  
  def category_name
    category.try(:name)
  end
  
  def category_name=(name)
    self.category = Category.find_by_name(name) || Category.new(name:) if name.present?
  end
end

// app/controllers/products_controller.rb
  
def product_params
  params.require(:product).permit(:name, :price, :category_name)
end
Wir haben Getter- und Setter-Methoden für "category_name" erstellt. Der Getter gibt den Namen der zugehörigen Kategorie zurück (aber beachten Sie, dass wir "try" verwenden, damit es "nil" zurückgibt, wenn keine zugehörige Kategorie vorhanden ist). Der Setter setzt die Kategorie des Produkts auf die Kategorie mit dem übereinstimmenden Namen, wenn der Name vorhanden ist.

2.png 620 KB


Wenn wir das Formular jetzt neu laden, haben wir ein Feld für den Kategorienamen. Wenn wir ein Produkt mit einem bereits vorhandenen Kategorienamen hinzufügen, wird das Produkt zur Datenbank hinzugefügt und die Zuordnung zu seiner Kategorie wird festgelegt.

3.png 614 KB



Was sollte jedoch passieren, wenn jemand ein Produkt mit einer neuen Kategorie erstellt? Es wäre gut, wenn die neue Kategorie in solchen Fällen zusammen mit dem Produkt erstellt würde, und Rails macht dies einfach möglich. Alles, was wir tun müssen, ist, find_by_name im Setter durch find_or_create_by zu ersetzen.
// app/model/product.rb  
def category_name=(name)
  self.category = Category.find_or_create_by(name:) if name.present?
end
Jetzt, wenn wir ein Produkt mit einer neuen Kategorie hinzufügen, wird die Kategorie ebenfalls erstellt.

Adding Auto-completion

Was wir wirklich tun möchten, ist die Autovervollständigung hinzufügen, damit während der Eingabe in das Kategorienfeld die Kategorien, die dem Eingegebenen entsprechen, angezeigt werden. Eine der einfachsten Möglichkeiten, dies in einer Rails 3.1-Anwendung zu tun, besteht darin, jQuery UI zu verwenden, das über ein Autocomplete-Widget verfügt. Daher werden wir diesen Ansatz verfolgen.

Mit Hilfe von den Befehlen
./bin/importmap pin jquery
./bin/importmap pin jquery-ui
richten wir die nötigen Anwendungen ein und können sie in der application.js importieren.
// app/javascript/application.js
import "jquery";
import "jquery-ui";
Dann erstellen wir eine Datei autocomplete_controller.js:
// app/javascript/controller/autocomplete_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    $(this.element).autocomplete({
      source: ["foo", "food", "four"],
    });
  }
}
In diesem Code überprüfen wir zunächst, ob der DOM geladen wurde. Wenn dies der Fall ist, suchen wir unser Kategorienamen-Feld anhand seiner ID und rufen die Autocomplete-Funktion auf. Diese Funktion nimmt eine Option namens "source" entgegen, die bestimmt, woher die Autocomplete-Optionen stammen. Wir können ihr entweder eine URL übergeben, was eine AJAX-Anfrage auslöst und das zurückgegebene Ergebnis verwendet, oder ein Array von Optionen. Um die Dinge schnell zum Laufen zu bringen, haben wir vorerst ein Array verwendet.

Wenn wir nun zur Seite für das Hinzufügen eines neuen Produkts gehen und etwas eingeben, das mit einem oder mehreren Elementen im Array übereinstimmt, sehen wir die Autocomplete-Liste.

4.png 634 KB



Die Autocomplete-Liste funktioniert, aber sie sieht schrecklich aus. Wir werden dies verbessern, indem wir den folgenden Code zur SCSS-Datei der Produkte hinzufügen.
//app/assets/stylesheets/products.css.scss

ul.ui-autocomplete {
  position: absolute;
  list-style: none;
  margin: 0;
  padding: 0;
  border: solid 1px #999;
  cursor: default;
  li {
    background-color: #FFF;
    border-top: solid 1px #DDD;
    margin: 0;
    padding: 1rem;
    a {
      color: #000;
      display: block;
      padding: 3px;
    }
    a.ui-state-hover, a.ui-state-active {
      background-color: #FFFCB2;
    }
  }
} 
Wenn wir die Seite jetzt neu laden, sieht die Liste viel besser aus.

5.png 621 KB

Autocomplete-Werte aus der Datenbank abrufen

Nun, da unsere Liste gut aussieht, können wir uns darauf konzentrieren, sie mit den übereinstimmenden Kategorien aus der Datenbank zu füllen, anstelle der Werte aus dem Array. Es gibt zwei Möglichkeiten, dies zu tun, und wir werden Ihnen beide Ansätze hier zeigen.

Die erste Option behält alles auf dem Client bei. Dies funktioniert gut, wenn es nicht viele Optionen zur Auswahl gibt, wie es hier der Fall ist. Was wir tun, ist, alle Optionen in einem Datenattribut auf dem Textfeld für den Kategorienamen einzubetten.
// app/views/products/_form.html.erb

<div data-controller="autocomplete">
  <%= form.label :category_name, style: "display: block" %>
  <%= form.text_field :category_name, data: {autocomplete_target: "categoryName",autocomplete_source: Category.order(:name).map(&:name)} %>
</div>
Um dies zu tun, setzen wir eine Datenoption auf das Textfeld (dieser Daten-Hash ist neu in Rails 3.1 und bietet eine bequeme Möglichkeit, Datenattribute zu setzen) und übergeben ihm ein Array von Kategorienamen. Wenn wir die Seite neu laden und den Quellcode anzeigen, sehen wir, was dies bewirkt hat.

Das Textfeld hat jetzt ein Datenattribut data-autocomplete-source, das die Kategorien enthält. Diese wurden in einen JSON-String umgewandelt und HTML-escaped. Jetzt können wir die Dummy-Daten in der Autocomplete-Funktion durch die Daten aus diesem Attribut ersetzen.
// app/javascript/controller/autocomplete_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["categoryName"];

  connect() {
    this.initializeAutocomplete();
  }

  initializeAutocomplete() {
    const $categoryName = $(this.categoryNameTarget);

    $categoryName.autocomplete({
      source: $categoryName.data("autocomplete-source"),
    });
  }
}
Wenn wir die Seite jetzt neu laden und Text im Feld für den Kategorienamen eingeben, erscheinen die passenden Kategorien in der Autocomplete-Liste.

6.png 618 KB


Dies ist alles, was wir tun müssen, um die Autocomplete-Funktion auf dem Client zum Laufen zu bringen, und dieser Ansatz funktioniert perfekt für uns, da wir nur eine begrenzte Anzahl von Kategorien haben. Es gibt jedoch andere Situationen, in denen wir Hunderte oder sogar Tausende von potenziellen Optionen haben könnten, und es wäre unpraktisch, jede Option auf dem Client zu haben. In solchen Fällen ist es besser, anstelle sie in das HTML-Dokument einzubetten, eine AJAX-Anfrage zu verwenden, um die Autocomplete-Optionen vom Server abzurufen.

Um dies zu tun, übergeben wir der Autocomplete-Funktion anstelle eines Arrays eine URL. Wir werden die Daten im Attribut data-autocomplete-source durch eine URL ersetzen. Da die AJAX-Anfrage eine Liste von Kategorien zurückgeben sollte, verwenden wir die URL categories_path.
<div data-controller="autocomplete">
  <%= form.label :category_name, style: "display: block" %>
  <%= form.text_field :category_name, data: {autocomplete_target: "categoryName",autocomplete_source: categories_path} %>
</div>
Jetzt werden wir in unserem neuen CategoriesController die Index-Action schreiben. Diese wird die Kategorien abrufen, die mit dem, was in das Textfeld eingegeben wurde, übereinstimmen, und sie als JSON-Daten zurückgeben.
// app/controllers/categories_controller.rb

class CategoriesController < ApplicationController
  before_action :set_category, only: %i[show edit update destroy]

  # GET /categories or /categories.json
  def index
    # @categories = Category.all
    @categories = Category.order(:name).where('name like ?', "#{params[:term]}%")
    render json: @categories.map(&:name)
  end
Das Autocomplete-Widget gibt den Text aus dem Textfeld als term-Parameter weiter, den wir dann verwenden, um die Kategorien zu filtern. Wir geben diese gefilterte Liste als Array von Namen zurück.

Wenn wir die Seite neu laden und nun im Kategorienamen-Feld zu tippen beginnen, werden die übereinstimmenden Kategorien vom Server abgerufen.

7.png 602 KB


Die Autocomplete-Liste ist etwas langsamer, da bei jeder Änderung des Texts im Textfeld eine AJAX-Anfrage gestellt wird. Allerdings müssen wir die vollständige Liste der Kategorien nicht mehr bei jedem Laden der Seite an den Client senden.

Noch kleine Korrekturen:
.ui-helper-hidden-accessible {
  visibility: hidden;
  display: none;
}

Ergebnis
:


8.png 592 KB



Zusätzliche gems:
  • gem 'jquery-rails'
  • gem 'jquery-ui-rails'
  • gem 'sassc-rails'
  • gem 'webpacker'
Meld dich an und schreibe ein Kommentar