Sortable Lists

Sortable Lists

Hier zeige ich, wie man jQuery UI verwendet, um eine sortierbare Liste zu erstellen, und eine "sortieren"-Aktion, um das Aktualisieren zu behandeln. Runden Sie es mit "acts_as_list" ab, um es vollständig funktionsfähig zu machen.

Eine Rails-App erstellen

$ rails new SortableList
Hier eine Faq-Liste erstellen.
$ rails g scaffold Faq question:text answer:text
$ rails db:migrate
Die faq index Seite etwas anpassen.
// app/views/faqs/index.html.erb

<p style="color: green"><%= notice %></p>

<h1>Faqs</h1>

<ul id="faqs">
  <% @faqs.each do |faq| %>
  <li>
    <%= link_to h(faq.question), faq %>
  </li>
  <% end %>
</ul>

<%= link_to "New faq", new_faq_path %>

Gemfile erweitern
.

gem 'jquery-rails'
gem 'jquery-ui-rails'
gem 'sassc-rails'
Unten ist eine Seite von einer Website, die eine Liste häufig gestellter Fragen zeigt. Wir möchten die Möglichkeit hinzufügen, die Elemente in der Liste durch Ziehen und Ablegen neu anzuordnen und die aktualisierte Reihenfolge in der Datenbank zu speichern, ohne die Seite neu laden zu müssen. In der Vergangenheit haben wir dies durch Verwendung des Sortable Helpers von Prototype und Scriptaculous erreicht, aber diese sind nicht mehr in Rails enthalten. Wie erreichen wir dies also in Rails?

1.png 652 KB

Einführung in jQuery UI

Anstelle von Prototype werden wir jQuery UI verwenden. Dieses wird mit einem Sortable-Plugin geliefert, das es einfach macht, eine Liste per Drag-and-Drop sortierbar zu machen. Das Plugin ist konfigurierbar und bietet eine Vielzahl von Optionen, die wir verwenden können, um das Verhalten an unsere Anwendung anzupassen.

Das Hinzufügen von  jQuery und jQuery UI zu unserer Rails-Anwendung ist einfach.
$ ./bin/importmap pin jquery
$ ./bin/importmap pin jquery-ui
und jetzt noch 2 Zeilen in die application.js.
// app/javascript/application.js

import "jquery";
import "jquery-ui";

Die Ansicht ändern

Wir müssen der Liste eine ID geben, damit wir darauf von JavaScript aus verweisen können. Außerdem müssen wir jedem Element in der Liste eine ID geben, damit wir es identifizieren können, wenn wir die aktualisierte Reihenfolge zurück an den Server senden, wenn wir die Elemente verschieben. Die Elemente müssen IDs im Format <model_name>_<model_id> haben, und wir können eine Hilfsmethode namens content_tag_for verwenden, um diese für uns zu generieren. Nachdem wir diese Änderungen vorgenommen haben, wird das Template so aussehen:
// app/views/faqs/index.html.erb

<p style="color: green"><%= notice %></p>

<h1>FAQs</h1>

<ul id="faqs">
  <% @faqs.each do |faq| %>
  <%= content_tag :li, faq do %>
  <%= link_to h(faq.question), faq %>
  <% end %>
  <% end %>
</ul>

<%= link_to "New faq", new_faq_path %>
Beachten Sie, dass es wichtig ist, dass der content_tag_for-Block ein Gleichheitszeichen in seinem Eröffnungstag hat, da er Inhalt ausgibt.

Sortable zur Liste hinzufügen

Als nächstes fügen wir das Skript hinzu, um die Liste sortierbar zu machen. Alles, was wir tun müssen, ist die Liste zu erhalten und sortable() darauf aufzurufen. Wir werden diesen Code in die jQuery-Funktion einbetten, damit er nur ausgeführt wird, wenn das DOM der Seite geladen wurde.
Erstellen eine Stimulus Controller.
// app/javascript/controllers/sortable_controller.js

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

export default class extends Controller {
  connect() {
    this.initializeSortable();
  }

  initializeSortable() {
    $(this.element).sortable();
  }
}
Und jetzt noch müssen wir den Controller noch aktivieren.
// app/views/faqs/index.html.erb

<p style="color: green"><%= notice %></p>

<h1>FAQs</h1>

<ul id="faqs" data-controller="sortable">
  <% @faqs.each do |faq| %>
  <%= content_tag :li, faq do %>
  <%= link_to h(faq.question), faq %>
  <% end %>
  <% end %>
</ul>

<%= link_to "New faq", new_faq_path %>
Wenn wir die Seite jetzt neu laden, können wir die Elemente in der Liste ziehen und neu anordnen.

Es gibt verschiedene Optionen, die wir an sortable übergeben können. Wir werden die axis-Option verwenden, damit die Liste nur vertikal sortiert wird, und auch einen update-Callback hinzufügen, der ausgelöst wird, wenn ein Element abgelegt wird.
// app/javascript/controllers/sortable_controller.js

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

export default class extends Controller {
  connect() {
    this.initializeSortable();
  }

  initializeSortable() {
    const updateCallback = () => {
      alert("updated!");
    };
    $(this.element).sortable({
      axis: "y",
      update: updateCallback,
    });
  }
}
Wenn wir jetzt ein Element ziehen, wird es nur vertikal verschoben, und wenn wir es ablegen, wird der Callback ausgelöst, und wir sehen einen Alert.

2.png 740 KB

Speichern der aktualisierten Reihenfolge

Wenn ein Element abgelegt wird, müssen wir einige Informationen an den Server senden, damit wir die aktualisierte Reihenfolge speichern können. In unserem aktuellen Stand der Anwendung wird es auf den Standard zurückgesetzt, wenn wir die Seite neu laden.

Das Faq-Modell benötigt ein position-Feld, um die Position jedes Elements zu speichern. Wir werden eine Migration dafür erstellen und dann rails db:migrate ausführen, um das neue Feld zur Datenbank hinzuzufügen.
$ rails g migration add_position_to_faqs position:integer
$ rails db:migrate
Als nächstes werden wir die index-Aktion des FaqsController aktualisieren, damit sie die FAQs in der Reihenfolge des position-Felds abruft.
// app/controllers/faqs_controller.rb
  
# GET /faqs or /faqs.json
  def index
    @faqs = Faq.order('position')
  end
Wenn sich die Reihenfolge der FAQs ändert, benötigen wir eine neue Aktion, um die Daten zu verarbeiten, die an den Server zurückgesendet werden. Der FaqsController hat die standardmäßigen sieben RESTful-Aktionen, aber keine davon tut genau das, was wir brauchen. Die update-Aktion kommt dem nahe, aber wir müssen mehrere Elemente auf einmal aktualisieren. Wir werden dem Controller eine sort-Aktion hinzufügen, die vorerst nichts rendert.
 // app/controllers/faqs_controller.rb

  def sort
    render nothing: true
  end
Wir müssen diese benutzerdefinierte Aktion zur Routendatei hinzufügen, im Ressourcenabschnitt für FAQs. Die sort-Aktion wird mit einer Anzahl von FAQs arbeiten, daher verwenden wir collection und post für sort. Es würde vielleicht mehr Sinn machen, hier put zu verwenden, aber post ist viel bequemer.
// app/config/routes.rb

Rails.application.routes.draw do
  get 'up' => 'rails/health#show', as: :rails_health_check

  root to: 'faqs#index'
  resources :faqs
  post '/faqs/sort', to: 'faqs#sort', as: 'sort_faqs'
end
Zurück in der js-Datei können wir den Code in der Rückruffunktion jetzt aktualisieren, sodass anstelle eines Alerts eine POST-Anfrage an unsere neue sort-Aktion gesendet wird. Es ist am besten, URLs nicht direkt in die js-Datei einzufügen, daher fügen wir die Callback-URL als neues Datenattribut in die Liste ein.
// app/views/faqs/index.html.erb

<ul id="faqs" data-controller="sortable" data-sortable-target="faq" data-update-url="<%= sort_faqs_path %>">
  <% @faqs.each do |faq| %>
  <%= content_tag :li, faq, 'data-faq-id' => faq.id do %>
  <span class="handle">[drag]
    <%= faq.id %>
  </span>
  <%= link_to h(faq.question), faq %>
  <%= faq.position %>
  <% end %>
  <% end %>
</ul>
Wir können diese URL in unserem stimulus-Code abrufen und sie verwenden, um die aktualisierten Positionsinformationen an die sort-Aktion zurückzusenden. Wir können diese Informationen abrufen, indem wir $(this).sortable('serialize') aufrufen. Dies wird alle Elemente in einem Format zusammenfassen, das wir an den Server senden können.
// app/javascript/controllers/sortable_controller.js

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

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

  connect() {
    this.initializeSortable();
  }

  initializeSortable() {
    $(this.faqTarget).sortable({
      axis: "y",
      handle: ".handle",
      update: this.handleSortUpdate.bind(this),
    });
  }

  handleSortUpdate() {
    const sortedIds = $(this.faqTarget).sortable("toArray", {
      attribute: "data-faq-id",
    });
    console.log(sortedIds);
    this.sendSortRequest(sortedIds);
  }

  sendSortRequest(sortedIds) {
    const updateUrl = $(this.faqTarget).data("update-url");
    $.post(updateUrl, { faq: sortedIds });
  }
}
Wenn wir die Seite jetzt neu laden und die Position eines Elements ändern, wird dies einen AJAX-Aufruf an die sort-Aktion auslösen. Wenn wir einen Blick in das Entwicklungstagebuch werfen, sehen wir, wie die Parameter als faq-Parameter mit einem Array von FAQ-IDs übergeben werden. Diese werden in der Reihenfolge gesendet, in der die aktualisierte Liste vorliegt.

In der sort-Aktion des FaqsController können wir nun diese Parameter lesen und die Position jedes FAQs aktualisieren, indem wir durch jedes davon mit seinem Index iterieren.

def sort
    sorted_ids = params[:faq].reject(&:blank?)

    sorted_ids.each_with_index do |id, index|
      faq = Faq.find(id)
      faq.update(position: index + 1)
    rescue ActiveRecord::RecordNotFound
      puts "FAQ with ID #{id} not found."
    end

    respond_to do |format|
      format.html
      format.json { render json: { message: 'Sort successful' } }
    end
  end
Wir müssen im Controller noch weitere Einträge vornehmen, die die Sicherheit angehen.
class CastsController < ApplicationController
  skip_before_action :verify_authenticity_token, only: [:sort]
  before_action :cast_sort, only: [:sort]

...

 private

  def faq_sort
    @faq = Faq.find(params[:id]) if params[:id].present?
  end

end

Wir werden auch den Griffen etwas Styling hinzufügen und ihnen einen Verschiebe-Cursor geben, um anzuzeigen, dass sie gezogen werden können.
// app/assets/stylesheets/_faqs.scss
#faqs .handle {
  font-size: 12px;
  color: #777;
  cursor: move;
}
Wenn wir die Seite jetzt neu laden, haben wir einen Griff für jedes Element, den wir verwenden können, um die Elemente zu verschieben.

3.png 615 KB

Hinzufügen eines neuen Elements
:

Wenn wir eine neue häufig gestellte Frage hinzufügen, wird sie ein null-Wert für das Position-Attribut haben. Es wäre besser, wenn das neue Element automatisch eine Position mit einem Wert erhalten würde, der es am Ende der Liste platziert.

Dies können wir erreichen, indem wir das Gem "acts_as_list" verwenden. Dieses Gem existiert schon seit einiger Zeit, wird jedoch aktiv von Swanand Pagnis gepflegt, sodass es sicher verwendet werden kann. Wie üblich fügen wir dieses Gem unserer Anwendung hinzu, indem wir es zur Gemfile hinzufügen und dann "bundle" ausführen, um es zu installieren.
// Gemfile

gem 'acts_as_list'

$ bundle install
Nachdem wir acts_as_list installiert haben, müssen wir es lediglich unserem Faq-Modell hinzufügen.
// app/models/faq.rb

class Faq < ApplicationRecord
  acts_as_list
end
Das Position-Attribut für alle neuen FAQs, die wir jetzt erstellen, wird automatisch festgelegt.
Meld dich an und schreibe ein Kommentar