用铁丝网构建拖放
#javascript #ruby #rails #hotwire

介绍

嘿,网络冒险家! ð构建Web应用程序可能是一个疯狂的旅程,对吗?好吧,今天,我们正在深入研究超级酷的东西:在您的Rails应用程序中添加拖放功能,所有这些功能都由Hotwire的魔力提供动力!不用担心,我们不会从头开始。 ð«

shhh ...我们的小秘密工具

我们已经有一个秘密武器:UpperBracket。这就像有一个魔术棒来生成全栈轨道应用。它带有所有的好东西 - vite,tailwind CSS,Rodauth,Rubocop等等,因此我们可以专注于有趣的东西。但是,让我们在我们之间保持这一点! ð

对最终结果感到好奇,这是

让我构建

那么,计划是什么?我们正在创建一个简单的课程列表应用程序,用户可以在其中用鼠标轻弹将项目重新排列。超级酷,对吧?让我们通过使用UpperBracket模板生成我们的Rails应用程序来开始问题。

# create app
rails new hotwire-dragndrop \
  -d postgresql \
  -m https://raw.githubusercontent.com/maful/upperbracket/main/template.rb

# move to the app directory
cd hotwire-dragndrop
  • 请确保您在潜水之前启动并运行了PostgreSQL数据库。

创建仅具有标题属性的课程模型,这里没有幻想。

rails g scaffold Course title

打开课程表的生成迁移,并定义标题不应为null。

class CreateCourses < ActiveRecord::Migration[7.0]
  def change
    create_table :courses do |t|
      t.string :title, null: false

      t.timestamps
    end
  end
end

在课程模型app/models/course.rb中添加title的存在验证

# frozen_string_literal: true

class Course < ApplicationRecord
  validates :title, presence: true
end

更新以将根页设置为课程列表页面的路由config/routes.rb

# frozen_string_literal: true

Rails.application.routes.draw do
  root "courses#index"
  resources :courses
end

我将用尾风CSS为页面添加一个小型样式,这是可选的。打开课程索引页app/views/courses/index.html.erb并使用以下代码更新

<div class="container">
  <div class="max-w-screen-md mx-auto py-10">
    <p style="color: green"><%= notice %></p>

    <div class="mb-6">
      <%= link_to "New course", new_course_path, class: "rounded border border-slate-500 px-2 py-3" %>
    </div>

    <h1 class="text-2xl font-medium mb-4">Courses</h1>

    <div id="courses" class="flex flex-col gap-4">
      <%= render @courses %>
    </div>
  </div>
</div>

打开app/views/courses/_course.html.erb中的单课程页面

<div
  id="<%= dom_id course %>"
  class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
>
  <div class="flex gap-4 items-center">
    <div class="text-gray-700 text-base">
      <%= course.title %>
    </div>
  </div>
</div>

运行rails db:migrate以运行数据库迁移。您现在可以使用bin/dev

运行该应用程序

在应用程序中添加一些课程,您现在应该有类似的东西

Initial Preview

您现在有一门课程列表,对于简单应用来说看起来很不错。但是,这不是结束,并注意到我们仍然无法拖放课程。

添加可排序的库

是时候添加主要功能拖放,添加ranked-model gem来处理后端中的记录订购

bundle add ranked-model

并添加可重新订购库和HTTP请求的节点软件包

yarn add sortablejs @rails/request.js

创建迁移以将row_order列添加到课程表

rails generate migration AddRowOrderToCourses row_order:integer

row_order列是无效的,我们已经在上一节中添加了一些课程。让我们填写课程表中现有记录的Row_order。

rails generate migration BackfillRowOrderToCourses

使用此代码更新生成的迁移文件

class BackfillRowOrderToCourses < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    Course.unscoped.in_batches do |relation|
      relation.update_all('row_order = EXTRACT(EPOCH FROM created_at)')
      sleep(0.01)
    end
  end
end

此迁移基本上是在此文件迁移过程中禁用DDL(数据定义语言),然后在课程中以批量记录进行迭代,并从created_at数据中更新row_order。通过运行rails db:migrate

执行新迁移

打开课程模型并包括排名模型的宝石并配置它。

# frozen_string_literal: true

class Course < ApplicationRecord
  include RankedModel

  ranks :row_order
end

现在,打开app/controllers/courses_controller.rb并在index方法中更新@courses变量,以将排名模型用于订购

def index
-  @courses = Course.all
+  @courses = Course.rank(:row_order).all
end

运行应用程序并检查课程页面。在UI的上下文中没有什么不同,但是如果您检查开发日志,您会注意到row_order已用于订购记录。

Course Load (1ms)  SELECT "courses".* FROM "courses" ORDER BY "courses"."row_order" ASC

创建可排序的刺激控制器

在上一节中,我们安装了sortable节点软件包,在本节中,我们将使用它。创建在app/javascript/controllers/sortable_controller.js中名为“ sortable”的刺激控制器,并添加以下代码

import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"

export default class extends Controller {
  connect() {
    const options = {
      onEnd: this.onEnd.bind(this)
    }
    Sortable.create(this.element, options)
  }

  onEnd(evt) {
    const body = { row_order_position: evt.newIndex }
    console.log(body)
  }
}

解释

  • 将sortablej导入控制器
  • 覆盖connect函数以创建当前元素的可排序实例。这意味着它将注册与可排序控制器连接的元素。
  • 想象您刚刚将项目拖到了新的位置。下一步是什么?我们添加了一个称为onEnd的函数。就像演出的大结局一样!但是,这是我们尚未将数据发送到后端的转折。相反,我们正在记录新索引以关注更改。

让我们尝试将刺激控制器连接到我们的元素。打开app/views/courses/index.html.erb文件,然后使用id courses
添加data-controller="sortable"

<div id="courses" class="flex flex-col gap-4" data-controller="sortable">

现在,让我们进行测试!启动您的应用程序,并给该课程列表一个旋转。您会注意到您可以拖放物品,而您拖动的项目将更改其位置。检查您的浏览器控制台以查看新的位置!

但是,当您刷新应用程序时,这就像键入倒带按钮一样。为什么?因为我们还没有将更改保存到数据库中。

该位置基于索引,而JavaScript中的索引从0开始。如果您在sortable_controller.js中再次检查,则body变量是我们需要发送到后端并将其保存到数据库中所需的。让我们这样做,在onEnd函数中,删除console.log行,因为我们不再需要它了。因此,这是现在的最终sortable_controller.js

我们正在谈论的职位基于索引。在JavaScript中快速抬头,该索引从0开始。在sortable_controller.js上窥视,您会发现body变量。那是我们需要发送到后端保存数据库的金色掘金。

onEnd函数中,我们对console.log系列说再见 - 我们不再需要它。这是我们抛光的sortable_controller.js

import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { patch } from "@rails/request.js"

export default class extends Controller {
  connect() {
    const options = {
      onEnd: this.onEnd.bind(this)
    }
    Sortable.create(this.element, options)
  }

  onEnd(evt) {
    const body = { row_order_position: evt.newIndex }
    patch(evt.item.dataset.sortableUrl, {
      body: JSON.stringify(body),
      responseKind: "turbo-stream",
    })
  }
}

哦,最后一件事 - 您会在代码中发现evt.item.dataset.sortableUrl。这就像一张地图要发送请求的位置。让我们通过打开路线并在课程资源中使用补丁方法添加等级来为其创建一条路线。

resources :courses do
  patch "rank", on: :member
end

打开courses_controller.rb并添加称为rank的新方法,这将在课程记录中更新row_order

def rank
  @course.update(row_order_position: params[:row_order_position])
end

不要忘记在顶部的before_action中包括rank方法

- before_action :set_course, only: [:show, :edit, :update, :destroy]
+ before_action :set_course, only: [:show, :edit, :update, :destroy, :rank]

打开app/views/courses/_course.html.erb并添加data-sortable-url属性,其中包含我们现在创建的路由

<div
  id="<%= dom_id course %>"
  class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
  data-sortable-url="<%= rank_course_path(course) %>"
>

如果您检查元素,则应该看到类似的东西

Inspect HTML Element

所以,代码evt.item.dataset.sortableUrl是在要拖动的项目上找到data-sortable-url的值。

厌倦了无尽的服务器配置头痛?包裹着为您服务。告别手动设置,并向无麻烦的部署打招呼。发现用包裹起来的红宝石应用程序的便利性并彻底改变了您的开发过程。开始使用WrappedBy

部署

让我们再试一次应用程序,立即在数据库中持续存在该位置的顺序。

奖金1

所以,您已经用铁轨中的热线钉了拖放,但是这是我们可以撒上很酷的UX升级。如何添加一个小标记以突出显示您的物品将在列表中落在列表中的位置?让我告诉你我的意思。打开您可信赖的sortable_controller.js并在connect函数中介绍ghostClass选项。

const options = {
  onEnd: this.onEnd.bind(this),
  ghostClass: "bg-red-300"
}

如果再次尝试该应用程序,您会在拖动项目的新位置上看到红色背景。

奖金2

但是,还有更多!这是您的另一个提示 - 如果您想对可以拖动哪些元素以及保留哪些元素的超级具体?好吧,这一切都是关于使用handle选择器。每个元素看起来都这样:

Illustration Bonus 2

首先,在课程标题的左侧添加移动图标。这是app/views/courses/_course.html.erb文件的完整

<div
  id="<%= dom_id course %>"
  class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
  data-sortable-url="<%= rank_course_path(course) %>"
>
  <div class="flex gap-4 items-center">
    <div class="sortable-handle cursor-grab">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-move h-4 w-4 text-current"><path d="m5 9-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20"/></svg>
    </div>
    <div class="text-gray-700 text-base">
      <%= course.title %>
    </div>
  </div>
</div>

打开app/views/courses/index.html.erb并在data-controller之后添加data-sortable-handle-selector-value属性

<div id="courses" class="flex flex-col gap-4" data-controller="sortable" data-sortable-handle-selector-value=".sortable-handle">

以及在sortable_controller.js中添加handle选择器并定义刺激值以存储data-sortable-handle-selector-value属性的值。

import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { patch } from "@rails/request.js"

export default class extends Controller {
  static values = {
    handleSelector: String,
  }

  connect() {
    const options = {
      onEnd: this.onEnd.bind(this),
      ghostClass: "bg-red-300"
    }
    if (this.hasHandleSelectorValue) {
      options.handle = this.handleSelectorValue
    }
    Sortable.create(this.element, options)
  }

  onEnd(evt) {
    const body = { row_order_position: evt.newIndex }
    patch(evt.item.dataset.sortableUrl, {
      body: JSON.stringify(body),
      responseKind: "turbo-stream",
    })
  }
}

让我们再次检查我们的应用程序的最终结果

请注意此处的区别,只有可以拖动的sortable-handle的元素,其余的元素回到普通元素。

结论

使用刺激,您拥有一个强大的工具,可以将应用程序的交互性提高到一个缺口。有关更多令人敬畏的功能和刺激巫师,请查看Stimulus Documentation

那是一个包裹,伙计们! ð我们成功地构建了一个光滑的列表,您可以像专业人士一样拖放,猜猜是什么?所有这些都贴在数据库中。但是,嘿,不要在这里停下来 - 您可以通过在每个课程中添加主题并让用户在不同课程中拖放主题来进一步进行。听起来很有趣,对吗?您可以使用sortablejs中的group选项来实现它。

可以在hotwire-sortable存储库上下载完整的源代码。


您是否厌倦了在铁路应用程序上部署Ruby的复杂性?发现包裹的红宝石爱好者的最终部署解决方案。好奇地看到它是如何工作的?在此处查看我们的YouTube视频: